diff --git a/kdevplatform/interfaces/iuicontroller.h b/kdevplatform/interfaces/iuicontroller.h index 09fa74e8e9..64c462fc61 100644 --- a/kdevplatform/interfaces/iuicontroller.h +++ b/kdevplatform/interfaces/iuicontroller.h @@ -1,170 +1,184 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * * * 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_IUICONTROLLER_H #define KDEVPLATFORM_IUICONTROLLER_H #include "interfacesexport.h" #include class QAction; namespace KParts { class MainWindow; } namespace Sublime{ class Controller; class View; class Area; + class Message; } namespace KDevelop { class IDocument; class IAssistant; class KDEVPLATFORMINTERFACES_EXPORT IToolViewFactory { public: virtual ~IToolViewFactory() {} /** * called to create a new widget for this tool view * @param parent the parent to use as parent for the widget * @returns the new widget for the tool view */ virtual QWidget* create(QWidget *parent = nullptr) = 0; /** * @returns the identifier of this tool view. The identifier * is used to remember which areas the tool view should appear * in, and must never change. */ virtual QString id() const = 0; /** * @returns the default position where this tool view should appear */ virtual Qt::DockWidgetArea defaultPosition() const = 0; /** * Fetch a list of actions to add to the toolbar of the tool view @p view * @param viewWidget the view to which the actions should be added * @returns a list of actions to be added to the toolbar */ virtual QList toolBarActions( QWidget* viewWidget ) const { return viewWidget->actions(); } /** * Fetch a list of actions to be shown in the context menu of the tool view @p view. * The default implementation will return all actions of @p viewWidget. * * @param viewWidget the view for which the context menu should be shown * @returns a list of actions to be shown in the context menu */ virtual QList contextMenuActions( QWidget* viewWidget ) const { return viewWidget->actions(); } /** * called when a new view is created from this template * @param view the new sublime view that is being shown */ virtual void viewCreated(Sublime::View* view); /** * @returns if multiple tool views can by created by this factory in the same area. */ virtual bool allowMultiple() const { return false; } }; /** * * Allows to access various parts of the user-interface, like the tool views or the mainwindow */ class KDEVPLATFORMINTERFACES_EXPORT IUiController { public: virtual ~IUiController(); enum SwitchMode { ThisWindow /**< indicates that the area switch should be in this window */, NewWindow /**< indicates that the area switch should be using a new window */ }; enum FindFlags { None = 0, Create = 1, ///The tool-view is created if it doesn't exist in the current area yet Raise = 2, ///The tool-view is raised if it was found/created CreateAndRaise = Create | Raise ///The tool view is created and raised }; virtual void switchToArea(const QString &areaName, SwitchMode switchMode) = 0; virtual void addToolView(const QString &name, IToolViewFactory *factory, FindFlags state = Create) = 0; virtual void removeToolView(IToolViewFactory *factory) = 0; /** Makes sure that this tool-view exists in the current area, raises it, and returns the contained widget * Returns zero on failure */ virtual QWidget* findToolView(const QString& name, IToolViewFactory *factory, FindFlags flags = CreateAndRaise) = 0; /** * Makes sure that the tool view that contains the widget @p toolViewWidget is visible to the user. */ virtual void raiseToolView(QWidget* toolViewWidget) = 0; /** @return active mainwindow or 0 if no such mainwindow is active.*/ virtual KParts::MainWindow *activeMainWindow() = 0; /*! @p status must implement KDevelop::IStatus */ virtual void registerStatus(QObject* status) = 0; /** * This is meant to be used by IDocument subclasses to initialize the * Sublime::Document. */ virtual Sublime::Controller* controller() = 0; /** Shows an error message in the status bar. * * Unlike all other functions in this class, this function is thread-safe. * You can call it from the background. * * @p message The message * @p timeout The timeout in seconds how long to show the message */ virtual void showErrorMessage(const QString& message, int timeout = 1) = 0; + // TODO: convert all these calls into postMessage + + /** + * Shows a message in the message area. + * + * If running in NoGui mode, the message will be discarded. + * + * Unlike all other functions in this class, this function is thread-safe. + * You can call it from the background. + * + * @p message the message, ownership is transferred + */ + virtual void postMessage(Sublime::Message* message) = 0; /** @return area for currently active sublime mainwindow or 0 if no sublime mainwindow is active.*/ virtual Sublime::Area *activeArea() = 0; /** * Widget which is currently responsible for consuming special events in the UI * (such as shortcuts) * * @sa IToolViewActionListener * @return QWidget implementing the IToolViewActionListener interface */ virtual QWidget* activeToolViewActionListener() const = 0; /** * @returns all areas in the shell * * @note there will be one per mainwindow, of each type, plus the default ones. */ virtual QList allAreas() const = 0; protected: IUiController(); }; } #endif diff --git a/kdevplatform/language/CMakeLists.txt b/kdevplatform/language/CMakeLists.txt index 8cadf4b734..53663fcddf 100644 --- a/kdevplatform/language/CMakeLists.txt +++ b/kdevplatform/language/CMakeLists.txt @@ -1,404 +1,405 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevplatform\") # Check whether malloc_trim(3) is supported. include(CheckIncludeFile) include(CheckSymbolExists) check_include_file("malloc.h" HAVE_MALLOC_H) check_symbol_exists(malloc_trim "malloc.h" HAVE_MALLOC_TRIM) # TODO: fix duchain/stringhelpers.cpp and drop this again remove_definitions( -DQT_NO_CAST_FROM_ASCII ) if(BUILD_TESTING) add_subdirectory(highlighting/tests) add_subdirectory(duchain/tests) add_subdirectory(backgroundparser/tests) add_subdirectory(codegen/tests) add_subdirectory(util/tests) endif() configure_file(${CMAKE_CURRENT_SOURCE_DIR}/language-features.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/language-features.h ) set(KDevPlatformLanguage_LIB_SRCS assistant/staticassistantsmanager.cpp assistant/renameaction.cpp assistant/renameassistant.cpp assistant/renamefileaction.cpp assistant/staticassistant.cpp editor/persistentmovingrangeprivate.cpp editor/persistentmovingrange.cpp editor/modificationrevisionset.cpp editor/modificationrevision.cpp backgroundparser/backgroundparser.cpp backgroundparser/parsejob.cpp backgroundparser/documentchangetracker.cpp backgroundparser/parseprojectjob.cpp backgroundparser/urlparselock.cpp duchain/specializationstore.cpp duchain/codemodel.cpp duchain/duchain.cpp duchain/waitforupdate.cpp duchain/duchainpointer.cpp duchain/ducontext.cpp duchain/indexedducontext.cpp duchain/indexedtopducontext.cpp duchain/localindexedducontext.cpp duchain/indexeddeclaration.cpp duchain/localindexeddeclaration.cpp duchain/topducontext.cpp duchain/topducontextdynamicdata.cpp duchain/topducontextutils.cpp duchain/functiondefinition.cpp duchain/declaration.cpp duchain/classmemberdeclaration.cpp duchain/classfunctiondeclaration.cpp duchain/classdeclaration.cpp duchain/use.cpp duchain/forwarddeclaration.cpp duchain/duchainbase.cpp duchain/duchainlock.cpp duchain/identifier.cpp duchain/parsingenvironment.cpp duchain/abstractfunctiondeclaration.cpp duchain/functiondeclaration.cpp duchain/stringhelpers.cpp duchain/namespacealiasdeclaration.cpp duchain/aliasdeclaration.cpp duchain/dumpdotgraph.cpp duchain/duchainutils.cpp duchain/declarationid.cpp duchain/definitions.cpp duchain/uses.cpp duchain/importers.cpp duchain/duchaindumper.cpp duchain/duchainregister.cpp duchain/persistentsymboltable.cpp duchain/instantiationinformation.cpp duchain/problem.cpp duchain/types/typesystem.cpp duchain/types/typeregister.cpp duchain/types/typerepository.cpp duchain/types/identifiedtype.cpp duchain/types/abstracttype.cpp duchain/types/integraltype.cpp duchain/types/functiontype.cpp duchain/types/structuretype.cpp duchain/types/pointertype.cpp duchain/types/referencetype.cpp duchain/types/delayedtype.cpp duchain/types/arraytype.cpp duchain/types/indexedtype.cpp duchain/types/enumerationtype.cpp duchain/types/constantintegraltype.cpp duchain/types/enumeratortype.cpp duchain/types/typeutils.cpp duchain/types/typealiastype.cpp duchain/types/unsuretype.cpp duchain/types/containertypes.cpp duchain/builders/dynamiclanguageexpressionvisitor.cpp duchain/navigation/problemnavigationcontext.cpp duchain/navigation/abstractnavigationwidget.cpp duchain/navigation/abstractnavigationcontext.cpp duchain/navigation/usesnavigationcontext.cpp duchain/navigation/abstractdeclarationnavigationcontext.cpp duchain/navigation/abstractincludenavigationcontext.cpp duchain/navigation/useswidget.cpp duchain/navigation/usescollector.cpp duchain/navigation/quickopenembeddedwidgetcombiner.cpp interfaces/abbreviations.cpp interfaces/iastcontainer.cpp interfaces/ilanguagesupport.cpp interfaces/quickopendataprovider.cpp interfaces/iquickopen.cpp interfaces/editorcontext.cpp interfaces/codecontext.cpp interfaces/icreateclasshelper.cpp interfaces/icontextbrowser.cpp codecompletion/codecompletion.cpp codecompletion/codecompletionworker.cpp codecompletion/codecompletionmodel.cpp codecompletion/codecompletionitem.cpp codecompletion/codecompletioncontext.cpp codecompletion/codecompletionitemgrouper.cpp codecompletion/codecompletionhelper.cpp codecompletion/normaldeclarationcompletionitem.cpp codegen/applychangeswidget.cpp codegen/coderepresentation.cpp codegen/documentchangeset.cpp codegen/duchainchangeset.cpp codegen/utilities.cpp codegen/codedescription.cpp codegen/basicrefactoring.cpp codegen/progressdialogs/refactoringdialog.cpp util/setrepository.cpp util/includeitem.cpp util/navigationtooltip.cpp highlighting/colorcache.cpp highlighting/configurablecolors.cpp highlighting/codehighlighting.cpp checks/dataaccessrepository.cpp checks/dataaccess.cpp checks/controlflowgraph.cpp checks/controlflownode.cpp classmodel/classmodel.cpp classmodel/classmodelnode.cpp classmodel/classmodelnodescontroller.cpp classmodel/allclassesfolder.cpp classmodel/documentclassesfolder.cpp classmodel/projectfolder.cpp codegen/templatesmodel.cpp codegen/templatepreviewicon.cpp codegen/templateclassgenerator.cpp codegen/sourcefiletemplate.cpp codegen/templaterenderer.cpp codegen/templateengine.cpp codegen/archivetemplateloader.cpp ) declare_qt_logging_category(KDevPlatformLanguage_LIB_SRCS TYPE LIBRARY CATEGORY_BASENAME "language" ) ki18n_wrap_ui(KDevPlatformLanguage_LIB_SRCS codegen/basicrefactoring.ui codegen/progressdialogs/refactoringdialog.ui) kdevplatform_add_library(KDevPlatformLanguage SOURCES ${KDevPlatformLanguage_LIB_SRCS}) target_include_directories(KDevPlatformLanguage PRIVATE ${Boost_INCLUDE_DIRS}) target_link_libraries(KDevPlatformLanguage PUBLIC KDev::Serialization KDev::Interfaces KDev::Util KF5::ThreadWeaver PRIVATE KDev::Project + KDev::Sublime KF5::GuiAddons KF5::TextEditor KF5::Parts KF5::Archive KF5::IconThemes Grantlee5::Templates ) install(FILES assistant/renameaction.h assistant/renameassistant.h assistant/staticassistant.h assistant/staticassistantsmanager.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/assistant COMPONENT Devel ) install(FILES interfaces/ilanguagesupport.h interfaces/icodehighlighting.h interfaces/quickopendataprovider.h interfaces/quickopenfilter.h interfaces/iquickopen.h interfaces/codecontext.h interfaces/editorcontext.h interfaces/iastcontainer.h interfaces/icreateclasshelper.h interfaces/icontextbrowser.h interfaces/abbreviations.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/interfaces COMPONENT Devel ) install(FILES editor/persistentmovingrange.h editor/documentrange.h editor/documentcursor.h editor/cursorinrevision.h editor/rangeinrevision.h editor/modificationrevision.h editor/modificationrevisionset.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/editor COMPONENT Devel ) install(FILES backgroundparser/backgroundparser.h backgroundparser/parsejob.h backgroundparser/parseprojectjob.h backgroundparser/urlparselock.h backgroundparser/documentchangetracker.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/backgroundparser COMPONENT Devel ) install(FILES util/navigationtooltip.h util/setrepository.h util/basicsetrepository.h util/includeitem.h util/debuglanguageparserhelper.h util/kdevhash.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/util COMPONENT Devel ) install(FILES duchain/parsingenvironment.h duchain/duchain.h duchain/codemodel.h duchain/ducontext.h duchain/ducontextdata.h duchain/topducontext.h duchain/topducontextutils.h duchain/topducontextdata.h duchain/declaration.h duchain/declarationdata.h duchain/classmemberdeclaration.h duchain/classmemberdeclarationdata.h duchain/classfunctiondeclaration.h duchain/classdeclaration.h duchain/functiondefinition.h duchain/use.h duchain/forwarddeclaration.h duchain/duchainbase.h duchain/duchainpointer.h duchain/duchainlock.h duchain/identifier.h duchain/abstractfunctiondeclaration.h duchain/functiondeclaration.h duchain/stringhelpers.h duchain/safetycounter.h duchain/namespacealiasdeclaration.h duchain/aliasdeclaration.h duchain/dumpdotgraph.h duchain/duchainutils.h duchain/duchaindumper.h duchain/declarationid.h duchain/appendedlist.h duchain/duchainregister.h duchain/persistentsymboltable.h duchain/instantiationinformation.h duchain/specializationstore.h duchain/indexedducontext.h duchain/indexedtopducontext.h duchain/localindexedducontext.h duchain/indexeddeclaration.h duchain/localindexeddeclaration.h duchain/definitions.h duchain/problem.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/duchain COMPONENT Devel ) install(FILES duchain/types/unsuretype.h duchain/types/identifiedtype.h duchain/types/typesystem.h duchain/types/typeregister.h duchain/types/typerepository.h duchain/types/typepointer.h duchain/types/typesystemdata.h duchain/types/abstracttype.h duchain/types/integraltype.h duchain/types/functiontype.h duchain/types/structuretype.h duchain/types/pointertype.h duchain/types/referencetype.h duchain/types/delayedtype.h duchain/types/arraytype.h duchain/types/indexedtype.h duchain/types/enumerationtype.h duchain/types/constantintegraltype.h duchain/types/enumeratortype.h duchain/types/alltypes.h duchain/types/typeutils.h duchain/types/typealiastype.h duchain/types/containertypes.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/duchain/types COMPONENT Devel ) install(FILES duchain/builders/abstractcontextbuilder.h duchain/builders/abstractdeclarationbuilder.h duchain/builders/abstracttypebuilder.h duchain/builders/abstractusebuilder.h duchain/builders/dynamiclanguageexpressionvisitor.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/duchain/builders COMPONENT Devel ) install(FILES codecompletion/codecompletion.h codecompletion/codecompletionworker.h codecompletion/codecompletionmodel.h codecompletion/codecompletionitem.h codecompletion/codecompletioncontext.h codecompletion/codecompletionitemgrouper.h codecompletion/codecompletionhelper.h codecompletion/normaldeclarationcompletionitem.h codecompletion/abstractincludefilecompletionitem.h codecompletion/codecompletiontesthelper.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/codecompletion COMPONENT Devel ) install(FILES codegen/applychangeswidget.h codegen/astchangeset.h codegen/duchainchangeset.h codegen/documentchangeset.h codegen/coderepresentation.h codegen/utilities.h codegen/templatesmodel.h codegen/templatepreviewicon.h codegen/templaterenderer.h codegen/templateengine.h codegen/sourcefiletemplate.h codegen/templateclassgenerator.h codegen/codedescription.h codegen/basicrefactoring.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/codegen COMPONENT Devel ) install(FILES duchain/navigation/usesnavigationcontext.h duchain/navigation/abstractnavigationcontext.h duchain/navigation/abstractdeclarationnavigationcontext.h duchain/navigation/abstractincludenavigationcontext.h duchain/navigation/abstractnavigationwidget.h duchain/navigation/navigationaction.h duchain/navigation/useswidget.h duchain/navigation/usescollector.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/duchain/navigation COMPONENT Devel ) install(FILES highlighting/codehighlighting.h highlighting/colorcache.h highlighting/configurablecolors.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/highlighting COMPONENT Devel ) install(FILES checks/dataaccess.h checks/dataaccessrepository.h checks/controlflowgraph.h checks/controlflownode.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/checks COMPONENT Devel ) install(FILES classmodel/classmodel.h classmodel/classmodelnode.h classmodel/classmodelnodescontroller.h classmodel/allclassesfolder.h classmodel/documentclassesfolder.h classmodel/projectfolder.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/classmodel COMPONENT Devel ) diff --git a/kdevplatform/language/assistant/renameaction.cpp b/kdevplatform/language/assistant/renameaction.cpp index ab24b0658b..86711d5502 100644 --- a/kdevplatform/language/assistant/renameaction.cpp +++ b/kdevplatform/language/assistant/renameaction.cpp @@ -1,124 +1,126 @@ /* Copyright 2012 Olivier de Gaalon 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 "renameaction.h" #include #include #include #include #include #include #include - -#include +#include +#include +// KF #include using namespace KDevelop; QVector RevisionedFileRanges::convert(const QMap>& uses) { QVector ret(uses.size()); auto insertIt = ret.begin(); for (auto it = uses.constBegin(); it != uses.constEnd(); ++it, ++insertIt) { insertIt->file = it.key(); insertIt->ranges = it.value(); DocumentChangeTracker* tracker = ICore::self()->languageController()->backgroundParser()->trackerForUrl(it.key()); if (tracker) { insertIt->revision = tracker->revisionAtLastReset(); } } return ret; } class KDevelop::RenameActionPrivate { public: Identifier m_oldDeclarationName; QString m_newDeclarationName; QVector m_oldDeclarationUses; }; RenameAction::RenameAction(const Identifier& oldDeclarationName, const QString& newDeclarationName, const QVector& oldDeclarationUses) : d_ptr(new RenameActionPrivate) { Q_D(RenameAction); d->m_oldDeclarationName = oldDeclarationName; d->m_newDeclarationName = newDeclarationName.trimmed(); d->m_oldDeclarationUses = oldDeclarationUses; } RenameAction::~RenameAction() { } QString RenameAction::description() const { Q_D(const RenameAction); return i18n("Rename \"%1\" to \"%2\"", d->m_oldDeclarationName.toString(), d->m_newDeclarationName); } QString RenameAction::newDeclarationName() const { Q_D(const RenameAction); return d->m_newDeclarationName; } QString RenameAction::oldDeclarationName() const { Q_D(const RenameAction); return d->m_oldDeclarationName.toString(); } void RenameAction::execute() { Q_D(RenameAction); DocumentChangeSet changes; for (const RevisionedFileRanges& ranges : qAsConst(d->m_oldDeclarationUses)) { for (const RangeInRevision range : ranges.ranges) { KTextEditor::Range currentRange; if (ranges.revision && ranges.revision->valid()) { currentRange = ranges.revision->transformToCurrentRevision(range); } else { currentRange = range.castToSimpleRange(); } DocumentChange useRename(ranges.file, currentRange, d->m_oldDeclarationName.toString(), d->m_newDeclarationName); changes.addChange(useRename); changes.setReplacementPolicy(DocumentChangeSet::WarnOnFailedChange); } } DocumentChangeSet::ChangeResult result = changes.applyAllChanges(); if (!result) { - KMessageBox::error(nullptr, i18n("Failed to apply changes: %1", result.m_failureReason)); + auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); } emit executed(this); } diff --git a/kdevplatform/language/assistant/renamefileaction.cpp b/kdevplatform/language/assistant/renamefileaction.cpp index 50ddab84ed..67ecf37ad0 100644 --- a/kdevplatform/language/assistant/renamefileaction.cpp +++ b/kdevplatform/language/assistant/renamefileaction.cpp @@ -1,90 +1,92 @@ /* Copyright 2012 Milian Wolff 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 "renamefileaction.h" #include #include #include #include +#include #include - +#include +// KF #include -#include using namespace KDevelop; class RenameFileActionPrivate { public: KDevelop::BasicRefactoring* m_refactoring; QUrl m_file; QString m_newName; }; RenameFileAction::RenameFileAction(BasicRefactoring* refactoring, const QUrl& file, const QString& newName) : d_ptr(new RenameFileActionPrivate) { Q_D(RenameFileAction); d->m_refactoring = refactoring; d->m_file = file; d->m_newName = newName; } RenameFileAction::~RenameFileAction() { } QString RenameFileAction::description() const { Q_D(const RenameFileAction); return i18n("Rename file from \"%1\" to \"%2\".", d->m_file.fileName(), d->m_refactoring->newFileName(d->m_file, d->m_newName)); } void RenameFileAction::execute() { Q_D(RenameFileAction); // save document to prevent unwanted dialogs IDocument* doc = ICore::self()->documentController()->documentForUrl(d->m_file); if (!doc) { qCWarning(LANGUAGE) << "could find no document for url:" << d->m_file; return; } if (!ICore::self()->documentController()->saveSomeDocuments(QList() << doc, IDocument::Silent)) { return; } // rename document DocumentChangeSet changes; DocumentChangeSet::ChangeResult result = d->m_refactoring->addRenameFileChanges(d->m_file, d->m_newName, &changes); if (result) { result = changes.applyAllChanges(); } if (!result) { - KMessageBox::error(nullptr, i18n("Failed to apply changes: %1", result.m_failureReason)); + auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); } emit executed(this); } diff --git a/kdevplatform/language/codegen/basicrefactoring.cpp b/kdevplatform/language/codegen/basicrefactoring.cpp index 7835077095..ff65b96968 100644 --- a/kdevplatform/language/codegen/basicrefactoring.cpp +++ b/kdevplatform/language/codegen/basicrefactoring.cpp @@ -1,376 +1,381 @@ /* This file is part of KDevelop * * Copyright 2014 Miquel Sabaté * * 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. */ // Qt #include // KF -#include #include #include #include // KDevelop #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include "progressdialogs/refactoringdialog.h" #include #include "ui_basicrefactoring.h" namespace { QPair splitFileAtExtension(const QString& fileName) { int idx = fileName.indexOf(QLatin1Char('.')); if (idx == -1) { return qMakePair(fileName, QString()); } return qMakePair(fileName.left(idx), fileName.mid(idx)); } } using namespace KDevelop; //BEGIN: BasicRefactoringCollector BasicRefactoringCollector::BasicRefactoringCollector(const IndexedDeclaration& decl) : UsesWidgetCollector(decl) { setCollectConstructors(true); setCollectDefinitions(true); setCollectOverloads(true); } QVector BasicRefactoringCollector::allUsingContexts() const { return m_allUsingContexts; } void BasicRefactoringCollector::processUses(KDevelop::ReferencedTopDUContext topContext) { m_allUsingContexts << IndexedTopDUContext(topContext.data()); UsesWidgetCollector::processUses(topContext); } //END: BasicRefactoringCollector //BEGIN: BasicRefactoring BasicRefactoring::BasicRefactoring(QObject* parent) : QObject(parent) { /* There's nothing to do here. */ } void BasicRefactoring::fillContextMenu(ContextMenuExtension& extension, Context* context, QWidget* parent) { auto* declContext = dynamic_cast(context); if (!declContext) return; DUChainReadLocker lock; Declaration* declaration = declContext->declaration().data(); if (declaration && acceptForContextMenu(declaration)) { QFileInfo finfo(declaration->topContext()->url().str()); if (finfo.isWritable()) { QAction* action = new QAction(i18n("Rename \"%1\"...", declaration->qualifiedIdentifier().toString()), parent); action->setData(QVariant::fromValue(IndexedDeclaration(declaration))); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); connect(action, &QAction::triggered, this, &BasicRefactoring::executeRenameAction); extension.addAction(ContextMenuExtension::RefactorGroup, action); } } } bool BasicRefactoring::shouldRenameUses(KDevelop::Declaration* declaration) const { // Now we know we're editing a declaration, but some declarations we don't offer a rename for // basically that's any declaration that wouldn't be fully renamed just by renaming its uses(). if (declaration->internalContext() || declaration->isForwardDeclaration()) { //make an exception for non-class functions if (!declaration->isFunctionDeclaration() || dynamic_cast(declaration)) return false; } return true; } QString BasicRefactoring::newFileName(const QUrl& current, const QString& newName) { QPair nameExtensionPair = splitFileAtExtension(current.fileName()); // if current file is lowercased, keep that if (nameExtensionPair.first == nameExtensionPair.first.toLower()) { return newName.toLower() + nameExtensionPair.second; } else { return newName + nameExtensionPair.second; } } DocumentChangeSet::ChangeResult BasicRefactoring::addRenameFileChanges(const QUrl& current, const QString& newName, DocumentChangeSet* changes) { return changes->addDocumentRenameChange( IndexedString(current), IndexedString(newFileName(current, newName))); } bool BasicRefactoring::shouldRenameFile(Declaration* declaration) { // only try to rename files when we renamed a class/struct if (!dynamic_cast(declaration)) { return false; } const QUrl currUrl = declaration->topContext()->url().toUrl(); const QString fileName = currUrl.fileName(); const QPair nameExtensionPair = splitFileAtExtension(fileName); // check whether we renamed something that is called like the document it lives in return nameExtensionPair.first.compare(declaration->identifier().toString(), Qt::CaseInsensitive) == 0; } DocumentChangeSet::ChangeResult BasicRefactoring::applyChanges(const QString& oldName, const QString& newName, DocumentChangeSet& changes, DUContext* context, int usedDeclarationIndex) { if (usedDeclarationIndex == std::numeric_limits::max()) return DocumentChangeSet::ChangeResult::successfulResult(); for (int a = 0; a < context->usesCount(); ++a) { const Use& use(context->uses()[a]); if (use.m_declarationIndex != usedDeclarationIndex) continue; if (use.m_range.isEmpty()) { qCDebug(LANGUAGE) << "found empty use"; continue; } DocumentChangeSet::ChangeResult result = changes.addChange(DocumentChange(context->url(), context->transformFromLocalRevision(use.m_range), oldName, newName)); if (!result) return result; } const auto childContexts = context->childContexts(); for (DUContext* child : childContexts) { DocumentChangeSet::ChangeResult result = applyChanges(oldName, newName, changes, child, usedDeclarationIndex); if (!result) return result; } return DocumentChangeSet::ChangeResult::successfulResult(); } DocumentChangeSet::ChangeResult BasicRefactoring::applyChangesToDeclarations(const QString& oldName, const QString& newName, DocumentChangeSet& changes, const QList& declarations) { for (auto& decl : declarations) { Declaration* declaration = decl.data(); if (!declaration) continue; if (declaration->range().isEmpty()) qCDebug(LANGUAGE) << "found empty declaration"; TopDUContext* top = declaration->topContext(); DocumentChangeSet::ChangeResult result = changes.addChange(DocumentChange(top->url(), declaration->rangeInCurrentRevision(), oldName, newName)); if (!result) return result; } return DocumentChangeSet::ChangeResult::successfulResult(); } KDevelop::IndexedDeclaration BasicRefactoring::declarationUnderCursor(bool allowUse) { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if (!view) return KDevelop::IndexedDeclaration(); KTextEditor::Document* doc = view->document(); DUChainReadLocker lock; if (allowUse) return DUChainUtils::itemUnderCursor(doc->url(), KTextEditor::Cursor(view->cursorPosition())).declaration; else return DUChainUtils::declarationInLine(KTextEditor::Cursor( view->cursorPosition()), DUChainUtils::standardContextForUrl(doc->url())); } void BasicRefactoring::startInteractiveRename(const KDevelop::IndexedDeclaration& decl) { DUChainReadLocker lock(DUChain::lock()); Declaration* declaration = decl.data(); if (!declaration) { - KMessageBox::error(ICore::self()->uiController()->activeMainWindow(), i18n("No declaration under cursor")); + auto* message = new Sublime::Message(i18n("No declaration under cursor"), Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return; } QFileInfo info(declaration->topContext()->url().str()); if (!info.isWritable()) { - KMessageBox::error(ICore::self()->uiController()->activeMainWindow(), - i18n("Declaration is located in non-writable file %1.", - declaration->topContext()->url().str())); + const QString messageText = i18n("Declaration is located in non-writable file %1.", + declaration->topContext()->url().str()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return; } QString originalName = declaration->identifier().identifier().str(); lock.unlock(); NameAndCollector nc = newNameForDeclaration(DeclarationPointer(declaration)); if (nc.newName == originalName || nc.newName.isEmpty()) return; renameCollectedDeclarations(nc.collector.data(), nc.newName, originalName); } bool BasicRefactoring::acceptForContextMenu(const Declaration* decl) { // Default implementation. Some language plugins might override it to // handle some cases. Q_UNUSED(decl); return true; } void BasicRefactoring::executeRenameAction() { auto* action = qobject_cast(sender()); if (action) { IndexedDeclaration decl = action->data().value(); if (!decl.isValid()) decl = declarationUnderCursor(); if (!decl.isValid()) return; startInteractiveRename(decl); } } BasicRefactoring::NameAndCollector BasicRefactoring::newNameForDeclaration( const KDevelop::DeclarationPointer& declaration) { DUChainReadLocker lock; if (!declaration) { return {}; } QSharedPointer collector(new BasicRefactoringCollector(declaration.data())); Ui::RenameDialog renameDialog; QDialog dialog; renameDialog.setupUi(&dialog); UsesWidget uses(declaration.data(), collector); //So the context-links work auto* navigationWidget = declaration->context()->createNavigationWidget(declaration.data()); if (navigationWidget) connect(&uses, &UsesWidget::navigateDeclaration, navigationWidget, &AbstractNavigationWidget::navigateDeclaration); QString declarationName = declaration->toString(); dialog.setWindowTitle(i18nc("Renaming some declaration", "Rename \"%1\"", declarationName)); renameDialog.edit->setText(declaration->identifier().identifier().str()); renameDialog.edit->selectAll(); renameDialog.tabWidget->addTab(&uses, i18n("Uses")); if (navigationWidget) renameDialog.tabWidget->addTab(navigationWidget, i18n("Declaration Info")); lock.unlock(); if (dialog.exec() != QDialog::Accepted) return {}; const auto text = renameDialog.edit->text().trimmed(); RefactoringProgressDialog refactoringProgress(i18n("Renaming \"%1\" to \"%2\"", declarationName, text), collector.data()); if (!collector->isReady()) { if (refactoringProgress.exec() != QDialog::Accepted) { // krazy:exclude=crashy return {}; } } //TODO: input validation return { text, collector }; } DocumentChangeSet BasicRefactoring::renameCollectedDeclarations(KDevelop::BasicRefactoringCollector* collector, const QString& replacementName, const QString& originalName, bool apply) { DocumentChangeSet changes; DUChainReadLocker lock; const auto allUsingContexts = collector->allUsingContexts(); for (const KDevelop::IndexedTopDUContext collected : allUsingContexts) { QSet hadIndices; const auto declarations = collector->declarations(); for (const IndexedDeclaration decl : declarations) { uint usedDeclarationIndex = collected.data()->indexForUsedDeclaration(decl.data(), false); if (hadIndices.contains(usedDeclarationIndex)) continue; hadIndices.insert(usedDeclarationIndex); DocumentChangeSet::ChangeResult result = applyChanges(originalName, replacementName, changes, collected.data(), usedDeclarationIndex); if (!result) { - KMessageBox::error(nullptr, i18n("Applying changes failed: %1", result.m_failureReason)); + auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return {}; } } } DocumentChangeSet::ChangeResult result = applyChangesToDeclarations(originalName, replacementName, changes, collector->declarations()); if (!result) { - KMessageBox::error(nullptr, i18n("Applying changes failed: %1", result.m_failureReason)); + auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return {}; } ///We have to ignore failed changes for now, since uses of a constructor or of operator() may be created on "(" parens changes.setReplacementPolicy(DocumentChangeSet::IgnoreFailedChange); if (!apply) { return changes; } result = changes.applyAllChanges(); if (!result) { - KMessageBox::error(nullptr, i18n("Applying changes failed: %1", result.m_failureReason)); + auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); } return {}; } //END: BasicRefactoring diff --git a/kdevplatform/language/duchain/navigation/usescollector.cpp b/kdevplatform/language/duchain/navigation/usescollector.cpp index 602f3c6dd6..3726e1d5bf 100644 --- a/kdevplatform/language/duchain/navigation/usescollector.cpp +++ b/kdevplatform/language/duchain/navigation/usescollector.cpp @@ -1,479 +1,478 @@ /* 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 "usescollector.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "../classmemberdeclaration.h" #include "../abstractfunctiondeclaration.h" #include "../functiondefinition.h" #include #include #include +#include #include using namespace KDevelop; ///@todo make this language-neutral static Identifier destructorForName(const Identifier& name) { QString str = name.identifier().str(); if (str.startsWith(QLatin1Char('~'))) return Identifier(str); return Identifier(QLatin1Char('~') + str); } ///@todo Only collect uses within currently loaded projects template void collectImporters(ImportanceChecker& checker, ParsingEnvironmentFile* current, QSet& visited, QSet& collected) { //Ignore proxy-contexts while collecting. Those build a parallel and much more complicated structure. if (current->isProxyContext()) return; if (visited.contains(current)) return; visited.insert(current); if (checker(current)) collected.insert(current); const auto importers = current->importers(); for (const ParsingEnvironmentFilePointer& importer : importers) { if (importer.data()) collectImporters(checker, importer.data(), visited, collected); else qCDebug(LANGUAGE) << "missing environment-file, strange"; } } ///The returned set does not include the file itself ///@param visited should be empty on each call, used to prevent endless recursion void allImportedFiles(ParsingEnvironmentFilePointer file, QSet& set, QSet& visited) { const auto imports = file->imports(); for (const ParsingEnvironmentFilePointer& import : imports) { if (!import) { qCDebug(LANGUAGE) << "warning: missing import"; continue; } if (!visited.contains(import)) { visited.insert(import); set.insert(import->url()); allImportedFiles(import, set, visited); } } } void UsesCollector::setCollectConstructors(bool process) { m_collectConstructors = process; } void UsesCollector::setProcessDeclarations(bool process) { m_processDeclarations = process; } void UsesCollector::setCollectOverloads(bool collect) { m_collectOverloads = collect; } void UsesCollector::setCollectDefinitions(bool collect) { m_collectDefinitions = collect; } QList UsesCollector::declarations() { return m_declarations; } bool UsesCollector::isReady() const { return m_waitForUpdate.size() == m_updateReady.size(); } bool UsesCollector::shouldRespectFile(const IndexedString& document) { return ( bool )ICore::self()->projectController()->findProjectForUrl(document.toUrl()) || ( bool )ICore::self()->documentController()->documentForUrl(document.toUrl()); } struct ImportanceChecker { explicit ImportanceChecker(UsesCollector& collector) : m_collector(collector) { } bool operator ()(ParsingEnvironmentFile* file) { return m_collector.shouldRespectFile(file->url()); } UsesCollector& m_collector; }; void UsesCollector::startCollecting() { DUChainReadLocker lock(DUChain::lock()); if (Declaration* decl = m_declaration.data()) { if (m_collectDefinitions) { if (auto* def = dynamic_cast(decl)) { //Jump from definition to declaration Declaration* declaration = def->declaration(); if (declaration) decl = declaration; } } ///Collect all overloads into "decls" QList decls; if (m_collectOverloads && decl->context()->owner() && decl->context()->type() == DUContext::Class) { //First find the overridden base, and then all overriders of that base. while (Declaration* overridden = DUChainUtils::overridden(decl)) decl = overridden; uint maxAllowedSteps = 10000; decls += DUChainUtils::overriders(decl->context()->owner(), decl, maxAllowedSteps); if (maxAllowedSteps == 10000) { ///@todo Fail! } } decls << decl; ///Collect all "parsed versions" or forward-declarations etc. here, into allDeclarations QSet allDeclarations; for (Declaration* overload : qAsConst(decls)) { m_declarations = DUChainUtils::collectAllVersions(overload); for (const IndexedDeclaration& d : qAsConst(m_declarations)) { if (!d.data() || d.data()->id() != overload->id()) continue; allDeclarations.insert(d); if (m_collectConstructors && d.data() && d.data()->internalContext() && d.data()->internalContext()->type() == DUContext::Class) { const QList constructors = d.data()->internalContext()->findLocalDeclarations( d.data()->identifier(), CursorInRevision::invalid(), nullptr, AbstractType::Ptr(), DUContext::OnlyFunctions); for (Declaration* constructor : constructors) { auto* classFun = dynamic_cast(constructor); if (classFun && classFun->isConstructor()) allDeclarations.insert(IndexedDeclaration(constructor)); } Identifier destructorId = destructorForName(d.data()->identifier()); const QList destructors = d.data()->internalContext()->findLocalDeclarations( destructorId, CursorInRevision::invalid(), nullptr, AbstractType::Ptr(), DUContext::OnlyFunctions); for (Declaration* destructor : destructors) { auto* classFun = dynamic_cast(destructor); if (classFun && classFun->isDestructor()) allDeclarations.insert(IndexedDeclaration(destructor)); } } } } ///Collect definitions for declarations if (m_collectDefinitions) { for (const IndexedDeclaration d : qAsConst(allDeclarations)) { Declaration* definition = FunctionDefinition::definition(d.data()); if (definition) { qCDebug(LANGUAGE) << "adding definition"; allDeclarations.insert(IndexedDeclaration(definition)); } } } m_declarations.clear(); ///Step 4: Copy allDeclarations into m_declarations, build top-context list, etc. QList candidateTopContexts; candidateTopContexts.reserve(allDeclarations.size()); m_declarations.reserve(allDeclarations.size()); for (const IndexedDeclaration d : qAsConst(allDeclarations)) { m_declarations << d; m_declarationTopContexts.insert(d.indexedTopContext()); //We only collect declarations with the same type here.. candidateTopContexts << d.indexedTopContext().data(); } ImportanceChecker checker(*this); QSet visited; QSet collected; qCDebug(LANGUAGE) << "count of source candidate top-contexts:" << candidateTopContexts.size(); ///We use ParsingEnvironmentFile to collect all the relevant importers, because loading those is very cheap, compared ///to loading a whole TopDUContext. if (decl->inSymbolTable()) { //The declaration can only be used from other contexts if it is in the symbol table for (const ReferencedTopDUContext& top : qAsConst(candidateTopContexts)) { if (top->parsingEnvironmentFile()) { collectImporters(checker, top->parsingEnvironmentFile().data(), visited, collected); //In C++, visibility is not handled strictly through the import-structure. //It may happen that an object is visible because of an earlier include. //We can not perfectly handle that, but we can at least handle it if the header includes //the header that contains the declaration. That header may be parsed empty due to header-guards, //but we still need to pick it up here. const QList allVersions = DUChain::self()->allEnvironmentFiles( top->url()); for (const ParsingEnvironmentFilePointer& version : allVersions) collectImporters(checker, version.data(), visited, collected); } } } KDevelop::ParsingEnvironmentFile* file = decl->topContext()->parsingEnvironmentFile().data(); if (!file) return; if (checker(file)) collected.insert(file); { QSet filteredCollected; QMap grepCache; // Filter the collected files by performing a grep for (ParsingEnvironmentFile* file : qAsConst(collected)) { IndexedString url = file->url(); QMap::iterator grepCacheIt = grepCache.find(url); if (grepCacheIt == grepCache.end()) { CodeRepresentation::Ptr repr = KDevelop::createCodeRepresentation(url); if (repr) { QVector found = repr->grep(decl->identifier().identifier().str()); grepCacheIt = grepCache.insert(url, !found.isEmpty()); } } if (grepCacheIt.value()) filteredCollected << file; } qCDebug(LANGUAGE) << "Collected contexts for full re-parse, before filtering: " << collected.size() << " after filtering: " << filteredCollected.size(); collected = filteredCollected; } ///We have all importers now. However since we can tell parse-jobs to also update all their importers, we only need to ///update the "root" top-contexts that open the whole set with their imports. QSet rootFiles; QSet allFiles; for (ParsingEnvironmentFile* importer : qAsConst(collected)) { QSet allImports; QSet visited; allImportedFiles(ParsingEnvironmentFilePointer(importer), allImports, visited); //Remove all files from the "root" set that are imported by this one ///@todo more intelligent rootFiles -= allImports; allFiles += allImports; allFiles.insert(importer->url()); rootFiles.insert(importer->url()); } emit maximumProgressSignal(rootFiles.size()); maximumProgress(rootFiles.size()); //If we used the AllDeclarationsContextsAndUsesRecursive flag here, we would compute way too much. This way we only //set the minimum-features selectively on the files we really require them on. for (ParsingEnvironmentFile* file : qAsConst(collected)) { m_staticFeaturesManipulated.insert(file->url()); } m_staticFeaturesManipulated.insert(decl->url()); const auto currentFeaturesManipulated = m_staticFeaturesManipulated; for (const IndexedString& file : currentFeaturesManipulated) { ParseJob::setStaticMinimumFeatures(file, TopDUContext::AllDeclarationsContextsAndUses); } m_waitForUpdate = rootFiles; for (const IndexedString& file : qAsConst(rootFiles)) { qCDebug(LANGUAGE) << "updating root file:" << file.str(); DUChain::self()->updateContextForUrl(file, TopDUContext::AllDeclarationsContextsAndUses, this); } } else { emit maximumProgressSignal(0); maximumProgress(0); } } void UsesCollector::maximumProgress(uint max) { Q_UNUSED(max); } UsesCollector::UsesCollector(IndexedDeclaration declaration) : m_declaration(declaration) , m_collectOverloads(true) , m_collectDefinitions(true) , m_collectConstructors(false) , m_processDeclarations(true) { } UsesCollector::~UsesCollector() { ICore::self()->languageController()->backgroundParser()->revertAllRequests(this); const auto currentFeaturesManipulated = m_staticFeaturesManipulated; for (const IndexedString& file : currentFeaturesManipulated) { ParseJob::unsetStaticMinimumFeatures(file, TopDUContext::AllDeclarationsContextsAndUses); } } void UsesCollector::progress(uint processed, uint total) { Q_UNUSED(processed); Q_UNUSED(total); } void UsesCollector::updateReady(const KDevelop::IndexedString& url, KDevelop::ReferencedTopDUContext topContext) { DUChainReadLocker lock(DUChain::lock()); if (!topContext) { qCDebug(LANGUAGE) << "failed updating" << url.str(); } else { if (topContext->parsingEnvironmentFile() && topContext->parsingEnvironmentFile()->isProxyContext()) { ///Use the attached content-context instead const auto importedParentContexts = topContext->importedParentContexts(); for (const DUContext::Import& import : importedParentContexts) { if (import.context(nullptr) && import.context(nullptr)->topContext()->parsingEnvironmentFile() && !import.context(nullptr)->topContext()->parsingEnvironmentFile()->isProxyContext()) { if ((import.context(nullptr)->topContext()->features() & TopDUContext::AllDeclarationsContextsAndUses)) { ReferencedTopDUContext newTop(import.context(nullptr)->topContext()); topContext = newTop; break; } } } if (topContext->parsingEnvironmentFile() && topContext->parsingEnvironmentFile()->isProxyContext()) { qCDebug(LANGUAGE) << "got bad proxy-context for" << url.str(); topContext = nullptr; } } } if (m_waitForUpdate.contains(url) && !m_updateReady.contains(url)) { m_updateReady << url; m_checked.clear(); emit progressSignal(m_updateReady.size(), m_waitForUpdate.size()); progress(m_updateReady.size(), m_waitForUpdate.size()); } if (!topContext || !topContext->parsingEnvironmentFile()) { qCDebug(LANGUAGE) << "bad top-context"; return; } if (!m_staticFeaturesManipulated.contains(url)) return; //Not interesting if (!(topContext->features() & TopDUContext::AllDeclarationsContextsAndUses)) { ///@todo With simplified environment-matching, the same file may have been imported multiple times, ///while only one of those was updated. We have to check here whether this file is just such an import, ///or whether we work on with it. ///@todo We will lose files that were edited right after their update here. qCWarning(LANGUAGE) << "WARNING: context" << topContext->url().str() << "does not have the required features!!"; - ICore::self()->uiController()->showErrorMessage(QLatin1String( - "Updating ") + - ICore::self()->projectController()->prettyFileName(topContext-> - url().toUrl(), - KDevelop:: - IProjectController - ::FormatPlain) + - QLatin1String(" failed!"), 5); + // TODO no i18n? + const QString messageText = QLatin1String("Updating ") + + ICore::self()->projectController()->prettyFileName(topContext->url().toUrl(), KDevelop::IProjectController::FormatPlain) + QLatin1String(" failed!"); + auto* message = new Sublime::Message(messageText, Sublime::Message::Warning); + message->setAutoHide(0); + ICore::self()->uiController()->postMessage(message); return; } if (topContext->parsingEnvironmentFile()->needsUpdate()) { qCWarning(LANGUAGE) << "WARNING: context" << topContext->url().str() << "is not up to date!"; - ICore::self()->uiController()->showErrorMessage(i18n("%1 still needs an update!", - ICore::self()->projectController()->prettyFileName( - topContext->url().toUrl(), - KDevelop - ::IProjectController::FormatPlain)), 5); + const auto prettyFileName = ICore::self()->projectController()->prettyFileName(topContext->url().toUrl(), KDevelop::IProjectController::FormatPlain); + const auto messageText = i18n("%1 still needs an update!", prettyFileName); + auto* message = new Sublime::Message(messageText, Sublime::Message::Warning); + message->setAutoHide(0); + ICore::self()->uiController()->postMessage(message); // return; } IndexedTopDUContext indexed(topContext.data()); if (m_checked.contains(indexed)) return; if (!topContext.data()) { qCDebug(LANGUAGE) << "updated top-context is zero:" << url.str(); return; } m_checked.insert(indexed); if (m_declaration.data() && ((m_processDeclarations && m_declarationTopContexts.contains(indexed)) || DUChainUtils::contextHasUse(topContext.data(), m_declaration.data()))) { if (!m_processed.contains(topContext->url())) { m_processed.insert(topContext->url()); lock.unlock(); emit processUsesSignal(topContext); processUses(topContext); lock.lock(); } } else { if (!m_declaration.data()) { qCDebug(LANGUAGE) << "declaration has become invalid"; } } QList imports; const auto importedParentContexts = topContext->importedParentContexts(); for (const DUContext::Import& imported : importedParentContexts) { if (imported.context(nullptr) && imported.context(nullptr)->topContext()) imports << KDevelop::ReferencedTopDUContext(imported.context(nullptr)->topContext()); } for (const KDevelop::ReferencedTopDUContext& import : qAsConst(imports)) { IndexedString url = import->url(); lock.unlock(); updateReady(url, import); lock.lock(); } } IndexedDeclaration UsesCollector::declaration() const { return m_declaration; } diff --git a/kdevplatform/project/CMakeLists.txt b/kdevplatform/project/CMakeLists.txt index 836ff7dfdf..bcc64b3977 100644 --- a/kdevplatform/project/CMakeLists.txt +++ b/kdevplatform/project/CMakeLists.txt @@ -1,86 +1,86 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevplatform\") set(KDevPlatformProject_LIB_SRCS projectutils.cpp projectmodel.cpp projectchangesmodel.cpp projectconfigskeleton.cpp importprojectjob.cpp builderjob.cpp projectbuildsetmodel.cpp projectitemlineedit.cpp helper.cpp projectproxymodel.cpp abstractfilemanagerplugin.cpp filemanagerlistjob.cpp projectfiltermanager.cpp interfaces/iprojectbuilder.cpp interfaces/iprojectfilemanager.cpp interfaces/ibuildsystemmanager.cpp interfaces/iprojectfilter.cpp interfaces/iprojectfilterprovider.cpp widgets/dependencieswidget.cpp ) declare_qt_logging_category(KDevPlatformProject_LIB_SRCS TYPE LIBRARY HEADER debug_project.h CATEGORY_BASENAME "project" ) declare_qt_logging_category(KDevPlatformProject_LIB_SRCS TYPE LIBRARY HEADER debug_filemanager.h CATEGORY_BASENAME "filemanager" ) ki18n_wrap_ui( KDevPlatformProject_LIB_SRCS widgets/dependencieswidget.ui) kdevplatform_add_library(KDevPlatformProject SOURCES ${KDevPlatformProject_LIB_SRCS}) target_link_libraries(KDevPlatformProject PUBLIC KDev::Interfaces KDev::Util # util/path.h KDev::Vcs PRIVATE - KDev::Interfaces KDev::Serialization + KDev::Sublime KF5::KIOWidgets Qt5::Concurrent ) if(BUILD_TESTING) if(BUILD_TESTING) add_subdirectory(tests) endif() endif() install(FILES interfaces/iprojectbuilder.h interfaces/iprojectfilemanager.h interfaces/ibuildsystemmanager.h interfaces/iprojectfilter.h interfaces/iprojectfilterprovider.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/project/interfaces COMPONENT Devel ) install(FILES projectutils.h importprojectjob.h projectchangesmodel.h projectconfigskeleton.h projectmodel.h projectconfigpage.h projectitemlineedit.h projectbuildsetmodel.h builderjob.h helper.h abstractfilemanagerplugin.h projectfiltermanager.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/project COMPONENT Devel ) install(FILES widgets/dependencieswidget.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/project/widgets COMPONENT Devel ) diff --git a/kdevplatform/project/helper.cpp b/kdevplatform/project/helper.cpp index 46a2e57d68..da56cb567d 100644 --- a/kdevplatform/project/helper.cpp +++ b/kdevplatform/project/helper.cpp @@ -1,236 +1,243 @@ /* This file is part of KDevelop Copyright 2010 Milian Wolff 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 "helper.h" #include "debug.h" #include "path.h" #include #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include +#include +#include using namespace KDevelop; bool KDevelop::removeUrl(const KDevelop::IProject* project, const QUrl& url, const bool isFolder) { qCDebug(PROJECT) << "Removing url:" << url << "from project" << project; QWidget* window = QApplication::activeWindow(); auto job = KIO::stat(url, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(job, window); if (!job->exec()) { qCWarning(PROJECT) << "tried to remove non-existing url:" << url << project << isFolder; return true; } IPlugin* vcsplugin=project->versionControlPlugin(); if(vcsplugin) { auto* vcs=vcsplugin->extension(); // We have a vcs and the file/folder is controller, need to make the rename through vcs if(vcs->isVersionControlled(url)) { VcsJob* job=vcs->remove(QList() << url); if(job) { return job->exec(); } } } //if we didn't find a VCS, we remove using KIO (if the file still exists, the vcs plugin might have simply deleted the url without returning a job auto deleteJob = KIO::del(url); KJobWidgets::setWindow(deleteJob, window); if (!deleteJob->exec() && url.isLocalFile() && (QFileInfo::exists(url.toLocalFile()))) { - KMessageBox::error( window, + const QString messageText = isFolder ? i18n( "Cannot remove folder %1.", url.toDisplayString(QUrl::PreferLocalFile) ) - : i18n( "Cannot remove file %1.", url.toDisplayString(QUrl::PreferLocalFile) ) ); + : i18n( "Cannot remove file %1.", url.toDisplayString(QUrl::PreferLocalFile) ); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return false; } return true; } bool KDevelop::removePath(const KDevelop::IProject* project, const KDevelop::Path& path, const bool isFolder) { return removeUrl(project, path.toUrl(), isFolder); } bool KDevelop::createFile(const QUrl& file) { auto statJob = KIO::stat(file, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); if (statJob->exec()) { - KMessageBox::error( QApplication::activeWindow(), - i18n( "The file %1 already exists.", file.toDisplayString(QUrl::PreferLocalFile) ) ); + const QString messageText = i18n("The file %1 already exists.", file.toDisplayString(QUrl::PreferLocalFile)); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return false; } { auto uploadJob = KIO::storedPut(QByteArray("\n"), file, -1); KJobWidgets::setWindow(uploadJob, QApplication::activeWindow()); if (!uploadJob->exec()) { - KMessageBox::error( QApplication::activeWindow(), - i18n( "Cannot create file %1.", file.toDisplayString(QUrl::PreferLocalFile) ) ); + const QString messageText = i18n("Cannot create file %1.", file.toDisplayString(QUrl::PreferLocalFile)); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return false; } } return true; } bool KDevelop::createFile(const KDevelop::Path& file) { return createFile(file.toUrl()); } bool KDevelop::createFolder(const QUrl& folder) { auto mkdirJob = KIO::mkdir(folder); KJobWidgets::setWindow(mkdirJob, QApplication::activeWindow()); if (!mkdirJob->exec()) { - KMessageBox::error( QApplication::activeWindow(), i18n( "Cannot create folder %1.", folder.toDisplayString(QUrl::PreferLocalFile) ) ); + const QString messageText = i18n("Cannot create folder %1.", folder.toDisplayString(QUrl::PreferLocalFile)); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return false; } return true; } bool KDevelop::createFolder(const KDevelop::Path& folder) { return createFolder(folder.toUrl()); } bool KDevelop::renameUrl(const KDevelop::IProject* project, const QUrl& oldname, const QUrl& newname) { bool wasVcsMoved = false; IPlugin* vcsplugin = project->versionControlPlugin(); if (vcsplugin) { auto* vcs = vcsplugin->extension(); // We have a vcs and the file/folder is controller, need to make the rename through vcs if (vcs->isVersionControlled(oldname)) { VcsJob* job = vcs->move(oldname, newname); if (job && !job->exec()) { return false; } wasVcsMoved = true; } } // Fallback for the case of no vcs, or not-vcs-managed file/folder // try to save-as the text document, so users can directly continue to work // on the renamed url as well as keeping the undo-stack intact IDocument* document = ICore::self()->documentController()->documentForUrl(oldname); if (document && document->textDocument()) { if (!document->textDocument()->saveAs(newname)) { return false; } if (!wasVcsMoved) { // unlink the old file removeUrl(project, oldname, false); } return true; } else if (!wasVcsMoved) { // fallback for non-textdocuments (also folders e.g.) KIO::CopyJob* job = KIO::move(oldname, newname); KJobWidgets::setWindow(job, QApplication::activeWindow()); bool success = job->exec(); if (success) { // save files that where opened in this folder under the new name Path oldBasePath(oldname); Path newBasePath(newname); const auto documents = ICore::self()->documentController()->openDocuments(); for (auto* doc : documents) { auto textDoc = doc->textDocument(); if (textDoc && oldname.isParentOf(doc->url())) { const auto path = Path(textDoc->url()); const auto relativePath = oldBasePath.relativePath(path); const auto newPath = Path(newBasePath, relativePath); textDoc->saveAs(newPath.toUrl()); } } } return success; } else { return true; } } bool KDevelop::renamePath(const KDevelop::IProject* project, const KDevelop::Path& oldName, const KDevelop::Path& newName) { return renameUrl(project, oldName.toUrl(), newName.toUrl()); } bool KDevelop::copyUrl(const KDevelop::IProject* project, const QUrl& source, const QUrl& target) { IPlugin* vcsplugin=project->versionControlPlugin(); if(vcsplugin) { auto* vcs=vcsplugin->extension(); // We have a vcs and the file/folder is controller, need to make the rename through vcs if(vcs->isVersionControlled(source)) { VcsJob* job=vcs->copy(source, target); if(job) { return job->exec(); } } } // Fallback for the case of no vcs, or not-vcs-managed file/folder auto job = KIO::copy(source, target); KJobWidgets::setWindow(job, QApplication::activeWindow()); return job->exec(); } bool KDevelop::copyPath(const KDevelop::IProject* project, const KDevelop::Path& source, const KDevelop::Path& target) { return copyUrl(project, source.toUrl(), target.toUrl()); } Path KDevelop::proposedBuildFolder(const Path& sourceFolder) { Path proposedBuildFolder; if (sourceFolder.path().contains(QLatin1String("/src/"))) { const QString srcBuildPath = sourceFolder.path().replace(QLatin1String("/src/"), QLatin1String("/build/")); Q_ASSERT(!srcBuildPath.isEmpty()); if (QDir(srcBuildPath).exists()) { proposedBuildFolder = Path(srcBuildPath); } } if (!proposedBuildFolder.isValid()) { proposedBuildFolder = Path(sourceFolder, QStringLiteral("build")); } return proposedBuildFolder; } diff --git a/kdevplatform/shell/documentcontroller.cpp b/kdevplatform/shell/documentcontroller.cpp index b50281f433..3eb062088c 100644 --- a/kdevplatform/shell/documentcontroller.cpp +++ b/kdevplatform/shell/documentcontroller.cpp @@ -1,1264 +1,1267 @@ /* This file is part of the KDE project Copyright 2002 Matthias Hoelzer-Kluepfel Copyright 2002 Bernd Gehrmann Copyright 2003 Roberto Raggi Copyright 2003-2008 Hamish Rodda Copyright 2003 Harald Fernengel Copyright 2003 Jens Dagerbo Copyright 2005 Adam Treat Copyright 2004-2007 Alexander Dymo Copyright 2007 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 "documentcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include "core.h" #include "mainwindow.h" #include "textdocument.h" #include "uicontroller.h" #include "partcontroller.h" #include "savedialog.h" #include "debug.h" #include #include #define EMPTY_DOCUMENT_URL i18n("Untitled") using namespace KDevelop; class KDevelop::DocumentControllerPrivate { public: struct OpenFileResult { QList urls; QString encoding; }; explicit DocumentControllerPrivate(DocumentController* c) : controller(c) , fileOpenRecent(nullptr) { } ~DocumentControllerPrivate() = default; // used to map urls to open docs QHash< QUrl, IDocument* > documents; QHash< QString, IDocumentFactory* > factories; struct HistoryEntry { HistoryEntry() {} HistoryEntry( const QUrl & u, const KTextEditor::Cursor& cursor ); QUrl url; KTextEditor::Cursor cursor; int id; }; void removeDocument(Sublime::Document *doc) { const QList urlsForDoc = documents.keys(qobject_cast(doc)); for (const QUrl& url : urlsForDoc) { qCDebug(SHELL) << "destroying document" << doc; documents.remove(url); } } OpenFileResult showOpenFile() const { QUrl dir; if ( controller->activeDocument() ) { dir = controller->activeDocument()->url().adjusted(QUrl::RemoveFilename); } else { const auto cfg = KSharedConfig::openConfig()->group("Open File"); dir = cfg.readEntry( "Last Open File Directory", Core::self()->projectController()->projectsBaseDirectory() ); } const auto caption = i18n("Open File"); const auto filter = i18n("*|Text File\n"); auto parent = Core::self()->uiControllerInternal()->defaultMainWindow(); // use special dialogs in a KDE session, native dialogs elsewhere if (qEnvironmentVariableIsSet("KDE_FULL_SESSION")) { const auto result = KEncodingFileDialog::getOpenUrlsAndEncoding(QString(), dir, filter, parent, caption); return {result.URLs, result.encoding}; } // note: can't just filter on text files using the native dialog, just display all files // see https://phabricator.kde.org/D622#11679 const auto urls = QFileDialog::getOpenFileUrls(parent, caption, dir); return {urls, QString()}; } void chooseDocument() { const auto res = showOpenFile(); if( !res.urls.isEmpty() ) { QString encoding = res.encoding; for (const QUrl& u : res.urls) { openDocumentInternal(u, QString(), KTextEditor::Range::invalid(), encoding ); } } } void changeDocumentUrl(KDevelop::IDocument* document) { QMutableHashIterator it = documents; while (it.hasNext()) { if (it.next().value() == document) { const auto documentIt = documents.constFind(document->url()); if (documentIt != documents.constEnd()) { // Weird situation (saving as a file that is already open) IDocument* origDoc = *documentIt; if (origDoc->state() & IDocument::Modified) { // given that the file has been saved, close the saved file as the other instance will become conflicted on disk document->close(); controller->activateDocument( origDoc ); break; } // Otherwise close the original document origDoc->close(); } else { // Remove the original document it.remove(); } documents.insert(document->url(), document); if (!controller->isEmptyDocumentUrl(document->url())) { fileOpenRecent->addUrl(document->url()); } break; } } } KDevelop::IDocument* findBuddyDocument(const QUrl &url, IBuddyDocumentFinder* finder) { const QList allDocs = controller->openDocuments(); for (KDevelop::IDocument* doc : allDocs) { if(finder->areBuddies(url, doc->url())) { return doc; } } return nullptr; } static bool fileExists(const QUrl& url) { if (url.isLocalFile()) { return QFile::exists(url.toLocalFile()); } else { auto job = KIO::stat(url, KIO::StatJob::SourceSide, 0, KIO::HideProgressInfo); KJobWidgets::setWindow(job, ICore::self()->uiController()->activeMainWindow()); return job->exec(); } }; IDocument* openDocumentInternal( const QUrl & inputUrl, const QString& prefName = QString(), const KTextEditor::Range& range = KTextEditor::Range::invalid(), const QString& encoding = QString(), DocumentController::DocumentActivationParams activationParams = {}, IDocument* buddy = nullptr) { Q_ASSERT(!inputUrl.isRelative()); Q_ASSERT(!inputUrl.fileName().isEmpty() || !inputUrl.isLocalFile()); QString _encoding = encoding; QUrl url = inputUrl; if ( url.isEmpty() && (!activationParams.testFlag(IDocumentController::DoNotCreateView)) ) { const auto res = showOpenFile(); if( !res.urls.isEmpty() ) url = res.urls.first(); _encoding = res.encoding; if ( url.isEmpty() ) //still no url return nullptr; } KSharedConfig::openConfig()->group("Open File").writeEntry( "Last Open File Directory", url.adjusted(QUrl::RemoveFilename) ); // clean it and resolve possible symlink url = url.adjusted( QUrl::NormalizePathSegments ); if ( url.isLocalFile() ) { QString path = QFileInfo( url.toLocalFile() ).canonicalFilePath(); if ( !path.isEmpty() ) url = QUrl::fromLocalFile( path ); } //get a part document IDocument* doc = documents.value(url); if (!doc) { QMimeType mimeType; if (DocumentController::isEmptyDocumentUrl(url)) { mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain")); } else if (!url.isValid()) { // Exit if the url is invalid (should not happen) // If the url is valid and the file does not already exist, // kate creates the file and gives a message saying so qCDebug(SHELL) << "invalid URL:" << url.url(); return nullptr; } else if (KProtocolInfo::isKnownProtocol(url.scheme()) && !fileExists(url)) { //Don't create a new file if we are not in the code mode. if (ICore::self()->uiController()->activeArea()->objectName() != QLatin1String("code")) { return nullptr; } // enfore text mime type in order to create a kate part editor which then can be used to create the file // otherwise we could end up opening e.g. okteta which then crashes, see: https://bugs.kde.org/id=326434 mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain")); } else { mimeType = QMimeDatabase().mimeTypeForUrl(url); if(!url.isLocalFile() && mimeType.isDefault()) { // fall back to text/plain, for remote files without extension, i.e. COPYING, LICENSE, ... // using a synchronous KIO::MimetypeJob is hazardous and may lead to repeated calls to // this function without it having returned in the first place // and this function is *not* reentrant, see assert below: // Q_ASSERT(!documents.contains(url) || documents[url]==doc); mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain")); } } // is the URL pointing to a directory? if (mimeType.inherits(QStringLiteral("inode/directory"))) { qCDebug(SHELL) << "cannot open directory:" << url.url(); return nullptr; } if( prefName.isEmpty() ) { // Try to find a plugin that handles this mimetype QVariantMap constraints; constraints.insert(QStringLiteral("X-KDevelop-SupportedMimeTypes"), mimeType.name()); Core::self()->pluginController()->pluginForExtension(QString(), QString(), constraints); } if( IDocumentFactory* factory = factories.value(mimeType.name())) { doc = factory->create(url, Core::self()); } if(!doc) { if( !prefName.isEmpty() ) { doc = new PartDocument(url, Core::self(), prefName); } else if ( Core::self()->partControllerInternal()->isTextType(mimeType)) { doc = new TextDocument(url, Core::self(), _encoding); } else if( Core::self()->partControllerInternal()->canCreatePart(url) ) { doc = new PartDocument(url, Core::self()); } else { int openAsText = KMessageBox::questionYesNo(nullptr, i18n("KDevelop could not find the editor for file '%1' of type %2.\nDo you want to open it as plain text?", url.fileName(), mimeType.name()), i18nc("@title:window", "Could Not Find Editor"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("AskOpenWithTextEditor")); if (openAsText == KMessageBox::Yes) doc = new TextDocument(url, Core::self(), _encoding); else return nullptr; } } } // The url in the document must equal the current url, else the housekeeping will get broken Q_ASSERT(!doc || doc->url() == url); if(doc && openDocumentInternal(doc, range, activationParams, buddy)) return doc; else return nullptr; } bool openDocumentInternal(IDocument* doc, const KTextEditor::Range& range, DocumentController::DocumentActivationParams activationParams, IDocument* buddy = nullptr) { IDocument* previousActiveDocument = controller->activeDocument(); KTextEditor::View* previousActiveTextView = ICore::self()->documentController()->activeTextDocumentView(); KTextEditor::Cursor previousActivePosition; if(previousActiveTextView) previousActivePosition = previousActiveTextView->cursorPosition(); QUrl url=doc->url(); UiController *uiController = Core::self()->uiControllerInternal(); Sublime::Area *area = uiController->activeArea(); //We can't have the same url in many documents //so we check it's already the same if it exists //contains=>it's the same Q_ASSERT(!documents.contains(url) || documents[url]==doc); auto *sdoc = dynamic_cast(doc); if( !sdoc ) { documents.remove(url); delete doc; return false; } //react on document deletion - we need to cleanup controller structures QObject::connect(sdoc, &Sublime::Document::aboutToDelete, controller, &DocumentController::notifyDocumentClosed); //We check if it was already opened before bool emitOpened = !documents.contains(url); if(emitOpened) documents[url]=doc; if (!activationParams.testFlag(IDocumentController::DoNotCreateView)) { //find a view if there's one already opened in this area Sublime::AreaIndex* activeViewIdx = area->indexOf(uiController->activeSublimeWindow()->activeView()); const auto& views = sdoc->views(); auto it = std::find_if(views.begin(), views.end(), [&](Sublime::View* view) { Sublime::AreaIndex* areaIdx = area->indexOf(view); return (areaIdx && areaIdx == activeViewIdx); }); Sublime::View* partView = (it != views.end()) ? *it : nullptr; bool addView = false; if (!partView) { //no view currently shown for this url partView = sdoc->createView(); addView = true; } if(addView) { // This code is never executed when restoring session on startup, // only when opening a file manually Sublime::View* buddyView = nullptr; bool placeAfterBuddy = true; if(Core::self()->uiControllerInternal()->arrangeBuddies() && !buddy && doc->mimeType().isValid()) { // If buddy is not set, look for a (usually) plugin which handles this URL's mimetype // and use its IBuddyDocumentFinder, if available, to find a buddy document QString mime = doc->mimeType().name(); IBuddyDocumentFinder* buddyFinder = IBuddyDocumentFinder::finderForMimeType(mime); if(buddyFinder) { buddy = findBuddyDocument(url, buddyFinder); if(buddy) { placeAfterBuddy = buddyFinder->buddyOrder(buddy->url(), doc->url()); } } } if(buddy) { auto* sublimeDocBuddy = dynamic_cast(buddy); if(sublimeDocBuddy) { Sublime::AreaIndex *pActiveViewIndex = area->indexOf(uiController->activeSublimeWindow()->activeView()); if(pActiveViewIndex) { // try to find existing View of buddy document in current active view's tab const auto& activeAreaViews = pActiveViewIndex->views(); const auto& buddyViews = sublimeDocBuddy->views(); auto it = std::find_if(activeAreaViews.begin(), activeAreaViews.end(), [&](Sublime::View* view) { return buddyViews.contains(view); }); if (it != activeAreaViews.end()) { buddyView = *it; } } } } // add view to the area if(buddyView && area->indexOf(buddyView)) { if(placeAfterBuddy) { // Adding new view after buddy view, simple case area->addView(partView, area->indexOf(buddyView), buddyView); } else { // First new view, then buddy view area->addView(partView, area->indexOf(buddyView), buddyView); // move buddyView tab after the new document area->removeView(buddyView); area->addView(buddyView, area->indexOf(partView), partView); } } else { // no buddy found for new document / plugin does not support buddies / buddy feature disabled Sublime::View *activeView = uiController->activeSublimeWindow()->activeView(); Sublime::UrlDocument *activeDoc = nullptr; IBuddyDocumentFinder *buddyFinder = nullptr; if(activeView) activeDoc = qobject_cast(activeView->document()); if(activeDoc && Core::self()->uiControllerInternal()->arrangeBuddies()) { QString mime = QMimeDatabase().mimeTypeForUrl(activeDoc->url()).name(); buddyFinder = IBuddyDocumentFinder::finderForMimeType(mime); } if(Core::self()->uiControllerInternal()->openAfterCurrent() && Core::self()->uiControllerInternal()->arrangeBuddies() && buddyFinder) { // Check if active document's buddy is directly next to it. // For example, we have the already-open tabs | *foo.h* | foo.cpp | , foo.h is active. // When we open a new document here (and the buddy feature is enabled), // we do not want to separate foo.h and foo.cpp, so we take care and avoid this. Sublime::AreaIndex *activeAreaIndex = area->indexOf(activeView); int pos = activeAreaIndex->views().indexOf(activeView); Sublime::View *afterActiveView = activeAreaIndex->views().value(pos+1, nullptr); Sublime::UrlDocument *activeDoc = nullptr, *afterActiveDoc = nullptr; if(activeView && afterActiveView) { activeDoc = qobject_cast(activeView->document()); afterActiveDoc = qobject_cast(afterActiveView->document()); } if(activeDoc && afterActiveDoc && buddyFinder->areBuddies(activeDoc->url(), afterActiveDoc->url())) { // don't insert in between of two buddies, but after them area->addView(partView, activeAreaIndex, afterActiveView); } else { // The active document's buddy is not directly after it // => no problem, insert after active document area->addView(partView, activeView); } } else { // Opening as last tab won't disturb our buddies // Same, if buddies are disabled, we needn't care about them. // this method places the tab according to openAfterCurrent() area->addView(partView, activeView); } } } if (!activationParams.testFlag(IDocumentController::DoNotActivate)) { uiController->activeSublimeWindow()->activateView( partView, !activationParams.testFlag(IDocumentController::DoNotFocus)); } if (!activationParams.testFlag(IDocumentController::DoNotAddToRecentOpen) && !controller->isEmptyDocumentUrl(url)) { fileOpenRecent->addUrl( url ); } if( range.isValid() ) { if (range.isEmpty()) doc->setCursorPosition( range.start() ); else doc->setTextSelection( range ); } } // Deferred signals, wait until it's all ready first if( emitOpened ) { emit controller->documentOpened( doc ); } if (!activationParams.testFlag(IDocumentController::DoNotActivate) && doc != controller->activeDocument()) emit controller->documentActivated( doc ); saveAll->setEnabled(true); revertAll->setEnabled(true); close->setEnabled(true); closeAll->setEnabled(true); closeAllOthers->setEnabled(true); KTextEditor::Cursor activePosition; if(range.isValid()) activePosition = range.start(); else if(KTextEditor::View* v = doc->activeTextView()) activePosition = v->cursorPosition(); if (doc != previousActiveDocument || activePosition != previousActivePosition) emit controller->documentJumpPerformed(doc, activePosition, previousActiveDocument, previousActivePosition); return true; } DocumentController* const controller; QPointer saveAll; QPointer revertAll; QPointer close; QPointer closeAll; QPointer closeAllOthers; KRecentFilesAction* fileOpenRecent; }; Q_DECLARE_TYPEINFO(KDevelop::DocumentControllerPrivate::HistoryEntry, Q_MOVABLE_TYPE); DocumentController::DocumentController( QObject *parent ) : IDocumentController( parent ) , d_ptr(new DocumentControllerPrivate(this)) { setObjectName(QStringLiteral("DocumentController")); QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/DocumentController"), this, QDBusConnection::ExportScriptableSlots ); connect(this, &DocumentController::documentUrlChanged, this, [this] (IDocument* document) { Q_D(DocumentController); d->changeDocumentUrl(document); }); if(!(Core::self()->setupFlags() & Core::NoUi)) setupActions(); } void DocumentController::initialize() { } void DocumentController::cleanup() { Q_D(DocumentController); if (d->fileOpenRecent) d->fileOpenRecent->saveEntries( KConfigGroup(KSharedConfig::openConfig(), "Recent Files" ) ); // Close all documents without checking if they should be saved. // This is because the user gets a chance to save them during MainWindow::queryClose. const auto documents = openDocuments(); for (IDocument* doc : documents) { doc->close(IDocument::Discard); } } DocumentController::~DocumentController() = default; void DocumentController::setupActions() { Q_D(DocumentController); KActionCollection* ac = Core::self()->uiControllerInternal()->defaultMainWindow()->actionCollection(); QAction* action; action = ac->addAction( QStringLiteral("file_open") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); ac->setDefaultShortcut(action, Qt::CTRL + Qt::Key_O ); action->setText(i18n( "&Open..." ) ); connect(action, &QAction::triggered, this, [this] { Q_D(DocumentController); d->chooseDocument(); } ); action->setToolTip( i18n( "Open file" ) ); action->setWhatsThis( i18n( "Opens a file for editing." ) ); d->fileOpenRecent = KStandardAction::openRecent(this, SLOT(slotOpenDocument(QUrl)), ac); d->fileOpenRecent->setWhatsThis(i18n("This lists files which you have opened recently, and allows you to easily open them again.")); d->fileOpenRecent->loadEntries( KConfigGroup(KSharedConfig::openConfig(), "Recent Files" ) ); action = d->saveAll = ac->addAction( QStringLiteral("file_save_all") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); action->setText(i18n( "Save Al&l" ) ); connect( action, &QAction::triggered, this, &DocumentController::slotSaveAllDocuments ); action->setToolTip( i18n( "Save all open documents" ) ); action->setWhatsThis( i18n( "Save all open documents, prompting for additional information when necessary." ) ); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_L) ); action->setEnabled(false); action = d->revertAll = ac->addAction( QStringLiteral("file_revert_all") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-revert"))); action->setText(i18n( "Reload All" ) ); connect( action, &QAction::triggered, this, &DocumentController::reloadAllDocuments ); action->setToolTip( i18n( "Revert all open documents" ) ); action->setWhatsThis( i18n( "Revert all open documents, returning to the previously saved state." ) ); action->setEnabled(false); action = d->close = ac->addAction( QStringLiteral("file_close") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); ac->setDefaultShortcut(action, Qt::CTRL + Qt::Key_W ); action->setText( i18n( "&Close" ) ); connect( action, &QAction::triggered, this, &DocumentController::fileClose ); action->setToolTip( i18n( "Close file" ) ); action->setWhatsThis( i18n( "Closes current file." ) ); action->setEnabled(false); action = d->closeAll = ac->addAction( QStringLiteral("file_close_all") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); action->setText(i18n( "Clos&e All" ) ); connect( action, &QAction::triggered, this, &DocumentController::closeAllDocuments ); action->setToolTip( i18n( "Close all open documents" ) ); action->setWhatsThis( i18n( "Close all open documents, prompting for additional information when necessary." ) ); action->setEnabled(false); action = d->closeAllOthers = ac->addAction( QStringLiteral("file_closeother") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); ac->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_W ); action->setText(i18n( "Close All Ot&hers" ) ); connect( action, &QAction::triggered, this, &DocumentController::closeAllOtherDocuments ); action->setToolTip( i18n( "Close all other documents" ) ); action->setWhatsThis( i18n( "Close all open documents, with the exception of the currently active document." ) ); action->setEnabled(false); action = ac->addAction( QStringLiteral("vcsannotate_current_document") ); connect( action, &QAction::triggered, this, &DocumentController::vcsAnnotateCurrentDocument ); action->setText( i18n( "Show Annotate on current document") ); action->setIconText( i18n( "Annotate" ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("user-properties")) ); } void DocumentController::slotOpenDocument(const QUrl &url) { openDocument(url); } IDocument* DocumentController::openDocumentFromText( const QString& data ) { IDocument* d = openDocument(nextEmptyDocumentUrl()); Q_ASSERT(d->textDocument()); d->textDocument()->setText( data ); return d; } bool DocumentController::openDocumentFromTextSimple( QString text ) { return (bool)openDocumentFromText( text ); } bool DocumentController::openDocumentSimple( QString url, int line, int column ) { return (bool)openDocument( QUrl::fromUserInput(url), KTextEditor::Cursor( line, column ) ); } IDocument* DocumentController::openDocument( const QUrl& inputUrl, const QString& prefName ) { Q_D(DocumentController); return d->openDocumentInternal( inputUrl, prefName ); } IDocument* DocumentController::openDocument( const QUrl & inputUrl, const KTextEditor::Range& range, DocumentActivationParams activationParams, const QString& encoding, IDocument* buddy) { Q_D(DocumentController); return d->openDocumentInternal(inputUrl, QString(), range, encoding, activationParams, buddy); } bool DocumentController::openDocument(IDocument* doc, const KTextEditor::Range& range, DocumentActivationParams activationParams, IDocument* buddy) { Q_D(DocumentController); return d->openDocumentInternal( doc, range, activationParams, buddy); } void DocumentController::fileClose() { IDocument *activeDoc = activeDocument(); if (activeDoc) { UiController *uiController = Core::self()->uiControllerInternal(); Sublime::View *activeView = uiController->activeSublimeWindow()->activeView(); uiController->activeArea()->closeView(activeView); } } bool DocumentController::closeDocument( const QUrl &url ) { Q_D(DocumentController); const auto documentIt = d->documents.constFind(url); if (documentIt == d->documents.constEnd()) return false; //this will remove all views and after the last view is removed, the //document will be self-destructed and removeDocument() slot will catch that //and clean up internal data structures (*documentIt)->close(); return true; } void DocumentController::notifyDocumentClosed(Sublime::Document* doc_) { Q_D(DocumentController); auto* doc = qobject_cast(doc_); Q_ASSERT(doc); d->removeDocument(doc_); if (d->documents.isEmpty()) { if (d->saveAll) d->saveAll->setEnabled(false); if (d->revertAll) d->revertAll->setEnabled(false); if (d->close) d->close->setEnabled(false); if (d->closeAll) d->closeAll->setEnabled(false); if (d->closeAllOthers) d->closeAllOthers->setEnabled(false); } emit documentClosed(doc); } IDocument * DocumentController::documentForUrl( const QUrl & dirtyUrl ) const { Q_D(const DocumentController); if (dirtyUrl.isEmpty()) { return nullptr; } Q_ASSERT(!dirtyUrl.isRelative()); Q_ASSERT(!dirtyUrl.fileName().isEmpty() || !dirtyUrl.isLocalFile()); //Fix urls that might not be normalized return d->documents.value( dirtyUrl.adjusted( QUrl::NormalizePathSegments ), nullptr ); } QList DocumentController::openDocuments() const { Q_D(const DocumentController); QList opened; for (IDocument* doc : qAsConst(d->documents)) { auto *sdoc = dynamic_cast(doc); if( !sdoc ) { continue; } if (!sdoc->views().isEmpty()) opened << doc; } return opened; } void DocumentController::activateDocument( IDocument * document, const KTextEditor::Range& range ) { // TODO avoid some code in openDocument? Q_ASSERT(document); openDocument(document->url(), range, IDocumentController::DoNotAddToRecentOpen); } void DocumentController::slotSaveAllDocuments() { saveAllDocuments(IDocument::Silent); } bool DocumentController::saveAllDocuments(IDocument::DocumentSaveMode mode) { return saveSomeDocuments(openDocuments(), mode); } bool KDevelop::DocumentController::saveSomeDocuments(const QList< IDocument * > & list, IDocument::DocumentSaveMode mode) { if (mode & IDocument::Silent) { const auto documents = modifiedDocuments(list); for (IDocument* doc : documents) { if( !DocumentController::isEmptyDocumentUrl(doc->url()) && !doc->save(mode) ) { if( doc ) qCWarning(SHELL) << "!! Could not save document:" << doc->url(); else qCWarning(SHELL) << "!! Could not save document as its NULL"; } // TODO if (!ret) showErrorDialog() ? } } else { // Ask the user which documents to save QList checkSave = modifiedDocuments(list); if (!checkSave.isEmpty()) { ScopedDialog dialog(checkSave, qApp->activeWindow()); return dialog->exec(); } } return true; } QList< IDocument * > KDevelop::DocumentController::visibleDocumentsInWindow(MainWindow * mw) const { // Gather a list of all documents which do have a view in the given main window // Does not find documents which are open in inactive areas QList list; const auto documents = openDocuments(); for (IDocument* doc : documents) { if (auto* sdoc = dynamic_cast(doc)) { const auto views = sdoc->views(); auto hasViewInWindow = std::any_of(views.begin(), views.end(), [&](Sublime::View* view) { return (view->hasWidget() && view->widget()->window() == mw); }); if (hasViewInWindow) { list.append(doc); } } } return list; } QList< IDocument * > KDevelop::DocumentController::documentsExclusivelyInWindow(MainWindow * mw, bool currentAreaOnly) const { // Gather a list of all documents which have views only in the given main window QList checkSave; const auto documents = openDocuments(); for (IDocument* doc : documents) { if (auto* sdoc = dynamic_cast(doc)) { bool inOtherWindow = false; const auto views = sdoc->views(); for (Sublime::View* view : views) { const auto windows = Core::self()->uiControllerInternal()->mainWindows(); for (Sublime::MainWindow* window : windows) { if(window->containsView(view) && (window != mw || (currentAreaOnly && window == mw && !mw->area()->views().contains(view)))) { inOtherWindow = true; break; } } if (inOtherWindow) { break; } } if (!inOtherWindow) checkSave.append(doc); } } return checkSave; } QList< IDocument * > KDevelop::DocumentController::modifiedDocuments(const QList< IDocument * > & list) const { QList< IDocument * > ret; for (IDocument* doc : list) { if (doc->state() == IDocument::Modified || doc->state() == IDocument::DirtyAndModified) ret.append(doc); } return ret; } bool DocumentController::saveAllDocumentsForWindow(KParts::MainWindow* mw, KDevelop::IDocument::DocumentSaveMode mode, bool currentAreaOnly) { QList checkSave = documentsExclusivelyInWindow(qobject_cast(mw), currentAreaOnly); return saveSomeDocuments(checkSave, mode); } void DocumentController::reloadAllDocuments() { if (Sublime::MainWindow* mw = Core::self()->uiControllerInternal()->activeSublimeWindow()) { const QList views = visibleDocumentsInWindow(qobject_cast(mw)); if (!saveSomeDocuments(views, IDocument::Default)) // User cancelled or other error return; for (IDocument* doc : views) { if(!isEmptyDocumentUrl(doc->url())) doc->reload(); } } } bool DocumentController::closeAllDocuments() { if (Sublime::MainWindow* mw = Core::self()->uiControllerInternal()->activeSublimeWindow()) { const QList views = visibleDocumentsInWindow(qobject_cast(mw)); if (!saveSomeDocuments(views, IDocument::Default)) // User cancelled or other error return false; for (IDocument* doc : views) { doc->close(IDocument::Discard); } } return true; } void DocumentController::closeAllOtherDocuments() { if (Sublime::MainWindow* mw = Core::self()->uiControllerInternal()->activeSublimeWindow()) { Sublime::View* activeView = mw->activeView(); if (!activeView) { qCWarning(SHELL) << "Shouldn't there always be an active view when this function is called?"; return; } // Deal with saving unsaved solo views QList soloViews = documentsExclusivelyInWindow(qobject_cast(mw)); soloViews.removeAll(qobject_cast(activeView->document())); if (!saveSomeDocuments(soloViews, IDocument::Default)) // User cancelled or other error return; const auto views = mw->area()->views(); for (Sublime::View* view : views) { if (view != activeView) mw->area()->closeView(view); } activeView->widget()->setFocus(); } } IDocument* DocumentController::activeDocument() const { UiController *uiController = Core::self()->uiControllerInternal(); Sublime::MainWindow* mw = uiController->activeSublimeWindow(); if( !mw || !mw->activeView() ) return nullptr; return qobject_cast(mw->activeView()->document()); } KTextEditor::View* DocumentController::activeTextDocumentView() const { UiController *uiController = Core::self()->uiControllerInternal(); Sublime::MainWindow* mw = uiController->activeSublimeWindow(); if( !mw || !mw->activeView() ) return nullptr; auto* view = qobject_cast(mw->activeView()); if(!view) return nullptr; return view->textView(); } QString DocumentController::activeDocumentPath( const QString& target ) const { if(!target.isEmpty()) { const auto projects = Core::self()->projectController()->projects(); for (IProject* project : projects) { if(project->name().startsWith(target, Qt::CaseInsensitive)) { return project->path().pathOrUrl() + QLatin1String("/."); } } } IDocument* doc = activeDocument(); if(!doc || target == QLatin1String("[selection]")) { Context* selection = ICore::self()->selectionController()->currentSelection(); if(selection && selection->type() == Context::ProjectItemContext && !static_cast(selection)->items().isEmpty()) { QString ret = static_cast(selection)->items().at(0)->path().pathOrUrl(); if(static_cast(selection)->items().at(0)->folder()) ret += QLatin1String("/."); return ret; } return QString(); } return doc->url().toString(); } QStringList DocumentController::activeDocumentPaths() const { UiController *uiController = Core::self()->uiControllerInternal(); if( !uiController->activeSublimeWindow() ) return QStringList(); QSet documents; const auto views = uiController->activeSublimeWindow()->area()->views(); for (Sublime::View* view : views) { documents.insert(view->document()->documentSpecifier()); } return documents.toList(); } void DocumentController::registerDocumentForMimetype( const QString& mimetype, KDevelop::IDocumentFactory* factory ) { Q_D(DocumentController); if( !d->factories.contains( mimetype ) ) d->factories[mimetype] = factory; } QStringList DocumentController::documentTypes() const { return QStringList() << QStringLiteral("Text"); } static const QRegularExpression& emptyDocumentPattern() { static const QRegularExpression pattern(QStringLiteral("^/%1(?:\\s\\((\\d+)\\))?$").arg(EMPTY_DOCUMENT_URL)); return pattern; } bool DocumentController::isEmptyDocumentUrl(const QUrl &url) { return emptyDocumentPattern().match(url.toDisplayString(QUrl::PreferLocalFile)).hasMatch(); } QUrl DocumentController::nextEmptyDocumentUrl() { int nextEmptyDocNumber = 0; const auto& pattern = emptyDocumentPattern(); const auto openDocuments = Core::self()->documentControllerInternal()->openDocuments(); for (IDocument* doc : openDocuments) { if (DocumentController::isEmptyDocumentUrl(doc->url())) { const auto match = pattern.match(doc->url().toDisplayString(QUrl::PreferLocalFile)); if (match.hasMatch()) { const int num = match.capturedRef(1).toInt(); nextEmptyDocNumber = qMax(nextEmptyDocNumber, num + 1); } else { nextEmptyDocNumber = qMax(nextEmptyDocNumber, 1); } } } QUrl url; if (nextEmptyDocNumber > 0) url = QUrl::fromLocalFile(QStringLiteral("/%1 (%2)").arg(EMPTY_DOCUMENT_URL).arg(nextEmptyDocNumber)); else url = QUrl::fromLocalFile(QLatin1Char('/') + EMPTY_DOCUMENT_URL); return url; } IDocumentFactory* DocumentController::factory(const QString& mime) const { Q_D(const DocumentController); return d->factories.value(mime); } bool DocumentController::openDocumentsSimple( QStringList urls ) { Sublime::Area* area = Core::self()->uiControllerInternal()->activeArea(); Sublime::AreaIndex* areaIndex = area->rootIndex(); QList topViews = static_cast(Core::self()->uiControllerInternal()->activeMainWindow())->topViews(); if(Sublime::View* activeView = Core::self()->uiControllerInternal()->activeSublimeWindow()->activeView()) areaIndex = area->indexOf(activeView); qCDebug(SHELL) << "opening " << urls << " to area " << area << " index " << areaIndex << " with children " << areaIndex->first() << " " << areaIndex->second(); bool isFirstView = true; bool ret = openDocumentsWithSplitSeparators( areaIndex, urls, isFirstView ); qCDebug(SHELL) << "area arch. after opening: " << areaIndex->print(); // Required because sublime sometimes doesn't update correctly when the area-index contents has been changed // (especially when views have been moved to other indices, through unsplit, split, etc.) static_cast(Core::self()->uiControllerInternal()->activeMainWindow())->reconstructViews(topViews); return ret; } bool DocumentController::openDocumentsWithSplitSeparators( Sublime::AreaIndex* index, QStringList urlsWithSeparators, bool& isFirstView ) { qCDebug(SHELL) << "opening " << urlsWithSeparators << " index " << index << " with children " << index->first() << " " << index->second() << " view-count " << index->viewCount(); if(urlsWithSeparators.isEmpty()) return true; Sublime::Area* area = Core::self()->uiControllerInternal()->activeArea(); QList topLevelSeparators; // Indices of the top-level separators (with groups skipped) const QStringList separators {QStringLiteral("/"), QStringLiteral("-")}; QList groups; bool ret = true; { int parenDepth = 0; int groupStart = 0; for(int pos = 0; pos < urlsWithSeparators.size(); ++pos) { QString item = urlsWithSeparators[pos]; if(separators.contains(item)) { if(parenDepth == 0) topLevelSeparators << pos; }else if(item == QLatin1String("[")) { if(parenDepth == 0) groupStart = pos+1; ++parenDepth; } else if(item == QLatin1String("]")) { if(parenDepth > 0) { --parenDepth; if(parenDepth == 0) groups << urlsWithSeparators.mid(groupStart, pos-groupStart); } else{ qCDebug(SHELL) << "syntax error in " << urlsWithSeparators << ": parens do not match"; ret = false; } }else if(parenDepth == 0) { groups << (QStringList() << item); } } } if(topLevelSeparators.isEmpty()) { if(urlsWithSeparators.size() > 1) { for (const QStringList& group : qAsConst(groups)) { ret &= openDocumentsWithSplitSeparators( index, group, isFirstView ); } }else{ while(index->isSplit()) index = index->first(); // Simply open the document into the area index IDocument* doc = Core::self()->documentControllerInternal()->openDocument(QUrl::fromUserInput(urlsWithSeparators.front()), KTextEditor::Cursor::invalid(), IDocumentController::DoNotActivate | IDocumentController::DoNotCreateView); auto *sublimeDoc = dynamic_cast(doc); if (sublimeDoc) { Sublime::View* view = sublimeDoc->createView(); area->addView(view, index); if(isFirstView) { static_cast(Core::self()->uiControllerInternal()->activeMainWindow())->activateView(view); isFirstView = false; } }else{ ret = false; } } return ret; } // Pick a separator in the middle int pickSeparator = topLevelSeparators[topLevelSeparators.size()/2]; bool activeViewToSecondChild = false; if(pickSeparator == urlsWithSeparators.size()-1) { // There is no right child group, so the right side should be filled with the currently active views activeViewToSecondChild = true; }else{ QStringList separatorsAndParens = separators; separatorsAndParens << QStringLiteral("[") << QStringLiteral("]"); // Check if the second child-set contains an unterminated separator, which means that the active views should end up there for(int pos = pickSeparator+1; pos < urlsWithSeparators.size(); ++pos) if( separators.contains(urlsWithSeparators[pos]) && (pos == urlsWithSeparators.size()-1 || separatorsAndParens.contains(urlsWithSeparators[pos-1])) ) activeViewToSecondChild = true; } Qt::Orientation orientation = urlsWithSeparators[pickSeparator] == QLatin1String("/") ? Qt::Horizontal : Qt::Vertical; if(!index->isSplit()) { qCDebug(SHELL) << "splitting " << index << "orientation" << orientation << "to second" << activeViewToSecondChild; index->split(orientation, activeViewToSecondChild); }else{ index->setOrientation(orientation); qCDebug(SHELL) << "WARNING: Area is already split (shouldn't be)" << urlsWithSeparators; } openDocumentsWithSplitSeparators( index->first(), urlsWithSeparators.mid(0, pickSeparator) , isFirstView ); if(pickSeparator != urlsWithSeparators.size() - 1) openDocumentsWithSplitSeparators( index->second(), urlsWithSeparators.mid(pickSeparator+1, urlsWithSeparators.size() - (pickSeparator+1) ), isFirstView ); // Clean up the child-indices, because document-loading may fail if(!index->first()->viewCount() && !index->first()->isSplit()) { qCDebug(SHELL) << "unsplitting first"; index->unsplit(index->first()); } else if(!index->second()->viewCount() && !index->second()->isSplit()) { qCDebug(SHELL) << "unsplitting second"; index->unsplit(index->second()); } return ret; } void DocumentController::vcsAnnotateCurrentDocument() { IDocument* doc = activeDocument(); if (!doc) return; QUrl url = doc->url(); IProject* project = KDevelop::ICore::self()->projectController()->findProjectForUrl(url); if(project && project->versionControlPlugin()) { auto* iface = project->versionControlPlugin()->extension(); auto helper = new VcsPluginHelper(project->versionControlPlugin(), iface); connect(doc->textDocument(), &KTextEditor::Document::aboutToClose, helper, QOverload::of(&VcsPluginHelper::disposeEventually)); Q_ASSERT(qobject_cast(doc->activeTextView())); // can't use new signal slot syntax here, AnnotationViewInterface is not a QObject connect(doc->activeTextView(), SIGNAL(annotationBorderVisibilityChanged(View*,bool)), helper, SLOT(disposeEventually(View*,bool))); helper->addContextDocument(url); helper->annotation(); } else { - KMessageBox::error(nullptr, i18n("Could not annotate the document because it is not " - "part of a version-controlled project.")); + const QString messageText = + i18n("Could not annotate the document because it is not part of a version-controlled project."); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); } } #include "moc_documentcontroller.cpp" diff --git a/kdevplatform/shell/project.cpp b/kdevplatform/shell/project.cpp index c1d8d9bb9f..bb01c01a52 100644 --- a/kdevplatform/shell/project.cpp +++ b/kdevplatform/shell/project.cpp @@ -1,712 +1,720 @@ /* This file is part of the KDE project Copyright 2001 Matthias Hoelzer-Kluepfel Copyright 2002-2003 Roberto Raggi Copyright 2002 Simon Hausmann Copyright 2003 Jens Dagerbo Copyright 2003 Mario Scalas Copyright 2003-2004 Alexander Dymo Copyright 2006 Matt Rogers Copyright 2007 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 "project.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include +#include #include #include #include #include "core.h" #include "mainwindow.h" #include "projectcontroller.h" #include "uicontroller.h" #include "debug.h" namespace KDevelop { class ProjectProgress : public QObject, public IStatus { Q_OBJECT Q_INTERFACES(KDevelop::IStatus) public: ProjectProgress(); ~ProjectProgress() override; QString statusName() const override; /*! Show indeterminate mode progress bar */ void setBuzzy(); /*! Hide progress bar */ void setDone(); QString projectName; private Q_SLOTS: void slotClean(); Q_SIGNALS: void clearMessage(KDevelop::IStatus*) override; void showMessage(KDevelop::IStatus*,const QString & message, int timeout = 0) override; void showErrorMessage(const QString & message, int timeout = 0) override; void hideProgress(KDevelop::IStatus*) override; void showProgress(KDevelop::IStatus*,int minimum, int maximum, int value) override; private: QTimer* m_timer; }; ProjectProgress::ProjectProgress() { m_timer = new QTimer(this); m_timer->setSingleShot( true ); m_timer->setInterval( 1000 ); connect(m_timer, &QTimer::timeout,this, &ProjectProgress::slotClean); } ProjectProgress::~ProjectProgress() { } QString ProjectProgress::statusName() const { return i18n("Loading Project %1", projectName); } void ProjectProgress::setBuzzy() { qCDebug(SHELL) << "showing busy progress" << statusName(); // show an indeterminate progressbar emit showProgress(this, 0,0,0); emit showMessage(this, i18nc("%1: Project name", "Loading %1", projectName)); } void ProjectProgress::setDone() { qCDebug(SHELL) << "showing done progress" << statusName(); // first show 100% bar for a second, then hide. emit showProgress(this, 0,1,1); m_timer->start(); } void ProjectProgress::slotClean() { emit hideProgress(this); emit clearMessage(this); } class ProjectPrivate { public: Path projectPath; Path projectFile; Path developerFile; QString developerTempFile; QTemporaryFile projectTempFile; IPlugin* manager = nullptr; QPointer vcsPlugin; ProjectFolderItem* topItem = nullptr; QString name; KSharedConfigPtr m_cfg; Project * const project; QSet fileSet; bool loading = false; bool fullReload; bool scheduleReload = false; ProjectProgress* progress; public: explicit ProjectPrivate(Project* project) : project(project) {} void reloadDone(KJob* job) { progress->setDone(); loading = false; ProjectController* projCtrl = Core::self()->projectControllerInternal(); if (job->error() == 0 && !Core::self()->shuttingDown()) { if(fullReload) projCtrl->projectModel()->appendRow(topItem); if (scheduleReload) { scheduleReload = false; project->reloadModel(); } } else { projCtrl->abortOpeningProject(project); } } QList itemsForPath( const IndexedString& path ) const { if ( path.isEmpty() ) { return QList(); } if (!topItem->model()) { // this gets hit when the project has not yet been added to the model // i.e. during import phase // TODO: should we handle this somehow? // possible idea: make the item<->path hash per-project return QList(); } Q_ASSERT(topItem->model()); QList items = topItem->model()->itemsForPath(path); QList::iterator it = items.begin(); while(it != items.end()) { if ((*it)->project() != project) { it = items.erase(it); } else { ++it; } } return items; } void importDone( KJob* job) { progress->setDone(); ProjectController* projCtrl = Core::self()->projectControllerInternal(); if(job->error() == 0 && !Core::self()->shuttingDown()) { loading=false; projCtrl->projectModel()->appendRow(topItem); projCtrl->projectImportingFinished( project ); } else { projCtrl->abortOpeningProject(project); } } void initProject(const Path& projectFile_) { // helper method for open() projectFile = projectFile_; } bool initProjectFiles() { KIO::StatJob* statJob = KIO::stat( projectFile.toUrl(), KIO::HideProgressInfo ); if ( !statJob->exec() ) //be sync for right now { - KMessageBox::sorry( Core::self()->uiControllerInternal()->defaultMainWindow(), - i18n( "Unable to load the project file %1.
" - "The project has been removed from the session.", - projectFile.pathOrUrl() ) ); + const QString messageText = + i18n("Unable to load the project file %1.
" + "The project has been removed from the session.", + projectFile.pathOrUrl()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return false; } // developerfile == dirname(projectFileUrl) ."/.kdev4/". basename(projectfileUrl) developerFile = projectFile; developerFile.setLastPathSegment( QStringLiteral(".kdev4") ); developerFile.addPath( projectFile.lastPathSegment() ); statJob = KIO::stat( developerFile.toUrl(), KIO::HideProgressInfo ); if( !statJob->exec() ) { // the developerfile does not exist yet, check if its folder exists // the developerfile itself will get created below QUrl dir = developerFile.parent().toUrl(); statJob = KIO::stat( dir, KIO::HideProgressInfo ); if( !statJob->exec() ) { KIO::SimpleJob* mkdirJob = KIO::mkdir( dir ); if( !mkdirJob->exec() ) { - KMessageBox::sorry( - Core::self()->uiController()->activeMainWindow(), + const QString messageText = i18n("Unable to create hidden dir (%1) for developer file", - dir.toDisplayString(QUrl::PreferLocalFile) ) - ); + dir.toDisplayString(QUrl::PreferLocalFile)); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return false; } } } projectTempFile.open(); auto copyJob = KIO::file_copy(projectFile.toUrl(), QUrl::fromLocalFile(projectTempFile.fileName()), -1, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(copyJob, Core::self()->uiController()->activeMainWindow()); if (!copyJob->exec()) { qCDebug(SHELL) << "Job failed:" << copyJob->errorString(); - KMessageBox::sorry( Core::self()->uiController()->activeMainWindow(), - i18n("Unable to get project file: %1", - projectFile.pathOrUrl() ) ); + const QString messageText = i18n("Unable to get project file: %1", projectFile.pathOrUrl()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return false; } if(developerFile.isLocalFile()) { developerTempFile = developerFile.toLocalFile(); } else { QTemporaryFile tmp; tmp.open(); developerTempFile = tmp.fileName(); auto job = KIO::file_copy(developerFile.toUrl(), QUrl::fromLocalFile(developerTempFile), -1, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(job, Core::self()->uiController()->activeMainWindow()); job->exec(); } return true; } KConfigGroup initKConfigObject() { // helper method for open() qCDebug(SHELL) << "Creating KConfig object for project files" << developerTempFile << projectTempFile.fileName(); m_cfg = KSharedConfig::openConfig( developerTempFile ); m_cfg->addConfigSources( QStringList() << projectTempFile.fileName() ); KConfigGroup projectGroup( m_cfg, "Project" ); return projectGroup; } bool projectNameUsed(const KConfigGroup& projectGroup) { // helper method for open() name = projectGroup.readEntry( "Name", projectFile.lastPathSegment() ); progress->projectName = name; if( Core::self()->projectController()->isProjectNameUsed( name ) ) { - KMessageBox::sorry( Core::self()->uiControllerInternal()->defaultMainWindow(), - i18n( "Could not load %1, a project with the same name '%2' is already open.", - projectFile.pathOrUrl(), name ) ); + const QString messageText = + i18n("Could not load %1, a project with the same name '%2' is already open.", projectFile.pathOrUrl(), name); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); qCWarning(SHELL) << "Trying to open a project with a name that is already used by another open project"; return true; } return false; } IProjectFileManager* fetchFileManager(const KConfigGroup& projectGroup) { if (manager) { auto* iface = manager->extension(); Q_ASSERT(iface); return iface; } // helper method for open() QString managerSetting = projectGroup.readEntry( "Manager", "KDevGenericManager" ); //Get our importer IPluginController* pluginManager = Core::self()->pluginController(); manager = pluginManager->pluginForExtension( QStringLiteral("org.kdevelop.IProjectFileManager"), managerSetting ); IProjectFileManager* iface = nullptr; if ( manager ) iface = manager->extension(); else { - KMessageBox::sorry( Core::self()->uiControllerInternal()->defaultMainWindow(), - i18n( "Could not load project management plugin %1.
Check that the required programs are installed," - " or see console output for more information.", - managerSetting ) ); + const QString messageText = + i18n("Could not load project management plugin %1.
Check that the required programs are installed," + " or see console output for more information.", managerSetting); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); manager = nullptr; return nullptr; } if (iface == nullptr) { - KMessageBox::sorry( Core::self()->uiControllerInternal()->defaultMainWindow(), - i18n( "project importing plugin (%1) does not support the IProjectFileManager interface.", managerSetting ) ); + const QString messageText = + i18n("The project importing plugin (%1) does not support the IProjectFileManager interface.", managerSetting); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); delete manager; manager = nullptr; } return iface; } void loadVersionControlPlugin(KConfigGroup& projectGroup) { // helper method for open() IPluginController* pluginManager = Core::self()->pluginController(); if( projectGroup.hasKey( "VersionControlSupport" ) ) { QString vcsPluginName = projectGroup.readEntry("VersionControlSupport", ""); if( !vcsPluginName.isEmpty() ) { vcsPlugin = pluginManager->pluginForExtension( QStringLiteral( "org.kdevelop.IBasicVersionControl" ), vcsPluginName ); } } else { const QList plugins = pluginManager->allPluginsForExtension( QStringLiteral( "org.kdevelop.IBasicVersionControl" ) ); for (IPlugin* p : plugins) { auto* iface = p->extension(); if (!iface) { continue; } const auto url = topItem->path().toUrl(); qCDebug(SHELL) << "Checking whether" << url << "is version controlled by" << iface->name(); if(iface->isVersionControlled(url)) { qDebug(SHELL) << "Detected that" << url << "is a" << iface->name() << "project"; vcsPlugin = p; projectGroup.writeEntry("VersionControlSupport", pluginManager->pluginInfo(p).pluginId()); projectGroup.sync(); } } } } bool importTopItem(IProjectFileManager* fileManager) { if (!fileManager) { return false; } topItem = fileManager->import( project ); if( !topItem ) { - KMessageBox::sorry( Core::self()->uiControllerInternal()->defaultMainWindow(), - i18n("Could not open project") ); + auto* message = new Sublime::Message(i18n("Could not open project."), Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return false; } return true; } }; Project::Project( QObject *parent ) : IProject( parent ) , d_ptr(new ProjectPrivate(this)) { Q_D(Project); d->progress = new ProjectProgress; Core::self()->uiController()->registerStatus( d->progress ); } Project::~Project() { Q_D(Project); delete d->progress; } QString Project::name() const { Q_D(const Project); return d->name; } QString Project::developerTempFile() const { Q_D(const Project); return d->developerTempFile; } QString Project::projectTempFile() const { Q_D(const Project); return d->projectTempFile.fileName(); } KSharedConfigPtr Project::projectConfiguration() const { Q_D(const Project); return d->m_cfg; } Path Project::path() const { Q_D(const Project); return d->projectPath; } void Project::reloadModel() { Q_D(Project); if (d->loading) { d->scheduleReload = true; return; } d->loading = true; d->fileSet.clear(); // delete topItem and remove it from model ProjectModel* model = Core::self()->projectController()->projectModel(); model->removeRow( d->topItem->row() ); d->topItem = nullptr; auto* iface = d->manager->extension(); if (!d->importTopItem(iface)) { d->loading = false; d->scheduleReload = false; return; } KJob* importJob = iface->createImportJob(d->topItem ); setReloadJob(importJob); d->fullReload = true; Core::self()->runController()->registerJob( importJob ); } void Project::setReloadJob(KJob* job) { Q_D(Project); d->loading = true; d->fullReload = false; d->progress->setBuzzy(); connect(job, &KJob::finished, this, [this] (KJob* job) { Q_D(Project); d->reloadDone(job); }); } bool Project::open( const Path& projectFile ) { Q_D(Project); d->initProject(projectFile); if (!d->initProjectFiles()) return false; KConfigGroup projectGroup = d->initKConfigObject(); if (d->projectNameUsed(projectGroup)) return false; d->projectPath = d->projectFile.parent(); IProjectFileManager* iface = d->fetchFileManager(projectGroup); if (!iface) return false; Q_ASSERT(d->manager); emit aboutToOpen(this); if (!d->importTopItem(iface) ) { return false; } d->loading=true; d->loadVersionControlPlugin(projectGroup); d->progress->setBuzzy(); KJob* importJob = iface->createImportJob(d->topItem ); connect(importJob, &KJob::result, this, [this] (KJob* job) { Q_D(Project); d->importDone(job); } ); Core::self()->runController()->registerJob( importJob ); return true; } void Project::close() { Q_D(Project); Q_ASSERT(d->topItem); if (d->topItem->row() == -1) { qCWarning(SHELL) << "Something went wrong. ProjectFolderItem detached. Project closed during reload?"; return; } Core::self()->projectController()->projectModel()->removeRow( d->topItem->row() ); if (!d->developerFile.isLocalFile()) { auto copyJob = KIO::file_copy(QUrl::fromLocalFile(d->developerTempFile), d->developerFile.toUrl(), -1, KIO::HideProgressInfo); KJobWidgets::setWindow(copyJob, Core::self()->uiController()->activeMainWindow()); if (!copyJob->exec()) { qCDebug(SHELL) << "Job failed:" << copyJob->errorString(); KMessageBox::sorry(Core::self()->uiController()->activeMainWindow(), i18n("Could not store developer specific project configuration.\n" "Attention: The project settings you changed will be lost.")); } } } bool Project::inProject( const IndexedString& path ) const { Q_D(const Project); if (d->fileSet.contains( path )) { return true; } return !d->itemsForPath( path ).isEmpty(); } QList< ProjectBaseItem* > Project::itemsForPath(const IndexedString& path) const { Q_D(const Project); return d->itemsForPath(path); } QList< ProjectFileItem* > Project::filesForPath(const IndexedString& file) const { Q_D(const Project); QList fileItems; const auto items = d->itemsForPath(file); for (ProjectBaseItem* item : items) { if( item->type() == ProjectBaseItem::File ) fileItems << static_cast(item); } return fileItems; } QList Project::foldersForPath(const IndexedString& folder) const { Q_D(const Project); QList folderItems; const auto items = d->itemsForPath(folder); for (ProjectBaseItem* item : items) { if( item->type() == ProjectBaseItem::Folder || item->type() == ProjectBaseItem::BuildFolder ) folderItems << static_cast(item); } return folderItems; } IProjectFileManager* Project::projectFileManager() const { Q_D(const Project); return d->manager->extension(); } IBuildSystemManager* Project::buildSystemManager() const { Q_D(const Project); return d->manager->extension(); } IPlugin* Project::managerPlugin() const { Q_D(const Project); return d->manager; } void Project::setManagerPlugin( IPlugin* manager ) { Q_D(Project); d->manager = manager; } Path Project::projectFile() const { Q_D(const Project); return d->projectFile; } Path Project::developerFile() const { Q_D(const Project); return d->developerFile; } ProjectFolderItem* Project::projectItem() const { Q_D(const Project); return d->topItem; } IPlugin* Project::versionControlPlugin() const { Q_D(const Project); return d->vcsPlugin.data(); } void Project::addToFileSet( ProjectFileItem* file ) { Q_D(Project); if (d->fileSet.contains(file->indexedPath())) { return; } d->fileSet.insert( file->indexedPath() ); emit fileAddedToSet( file ); } void Project::removeFromFileSet( ProjectFileItem* file ) { Q_D(Project); QSet::iterator it = d->fileSet.find(file->indexedPath()); if (it == d->fileSet.end()) { return; } d->fileSet.erase( it ); emit fileRemovedFromSet( file ); } QSet Project::fileSet() const { Q_D(const Project); return d->fileSet; } bool Project::isReady() const { Q_D(const Project); return !d->loading; } } // namespace KDevelop #include "project.moc" #include "moc_project.cpp" diff --git a/kdevplatform/shell/projectcontroller.cpp b/kdevplatform/shell/projectcontroller.cpp index 47f9d19367..91435623a2 100644 --- a/kdevplatform/shell/projectcontroller.cpp +++ b/kdevplatform/shell/projectcontroller.cpp @@ -1,1367 +1,1375 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 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 "projectcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include "core.h" // TODO: Should get rid off this include (should depend on IProject only) #include "project.h" #include "mainwindow.h" #include "shellextension.h" #include "plugincontroller.h" #include "configdialog.h" #include "uicontroller.h" #include "documentcontroller.h" #include "openprojectdialog.h" #include "sessioncontroller.h" #include "session.h" #include "debug.h" namespace KDevelop { class ProjectControllerPrivate { public: QList m_projects; QMap< IProject*, QList > m_projectPlugins; QPointer m_recentProjectsAction; Core* const m_core; // IProject* m_currentProject; ProjectModel* const model; QPointer m_openProject; QPointer m_fetchProject; QPointer m_closeProject; QPointer m_openConfig; IProjectDialogProvider* dialog; QList m_currentlyOpening; // project-file urls that are being opened ProjectController* const q; ProjectBuildSetModel* buildset; bool m_foundProjectFile; //Temporary flag used while searching the hierarchy for a project file bool m_cleaningUp; //Temporary flag enabled while destroying the project-controller ProjectChangesModel* m_changesModel = nullptr; QHash< IProject*, QPointer > m_parseJobs; // parse jobs that add files from the project to the background parser. ProjectControllerPrivate(Core* core, ProjectController* p) : m_core(core) , model(new ProjectModel()) , dialog(nullptr) , q(p) , buildset(nullptr) , m_foundProjectFile(false) , m_cleaningUp(false) { } void unloadAllProjectPlugins() { if( m_projects.isEmpty() ) m_core->pluginControllerInternal()->unloadProjectPlugins(); } void projectConfig( QObject * obj ) { if( !obj ) return; auto* proj = qobject_cast(obj); if( !proj ) return; auto cfgDlg = new KDevelop::ConfigDialog(m_core->uiController()->activeMainWindow()); cfgDlg->setAttribute(Qt::WA_DeleteOnClose); cfgDlg->setModal(true); QVector configPages; ProjectConfigOptions options; options.developerFile = proj->developerFile(); options.developerTempFile = proj->developerTempFile(); options.projectTempFile = proj->projectTempFile(); options.project = proj; const auto plugins = findPluginsForProject(proj); for (IPlugin* plugin : plugins) { const int perProjectConfigPagesCount = plugin->perProjectConfigPages(); configPages.reserve(configPages.size() + perProjectConfigPagesCount); for (int i = 0; i < perProjectConfigPagesCount; ++i) { configPages.append(plugin->perProjectConfigPage(i, options, cfgDlg)); } } std::sort(configPages.begin(), configPages.end(), [](const ConfigPage* a, const ConfigPage* b) { return a->name() < b->name(); }); for (auto page : configPages) { cfgDlg->appendConfigPage(page); } QObject::connect(cfgDlg, &ConfigDialog::configSaved, cfgDlg, [this, proj](ConfigPage* page) { Q_UNUSED(page) Q_ASSERT_X(proj, Q_FUNC_INFO, "ConfigDialog signalled project config change, but no project set for configuring!"); emit q->projectConfigurationChanged(proj); }); cfgDlg->setWindowTitle(i18n("Configure Project %1", proj->name())); QObject::connect(cfgDlg, &KDevelop::ConfigDialog::finished, proj, [proj]() { proj->projectConfiguration()->sync(); }); cfgDlg->show(); } void saveListOfOpenedProjects() { auto activeSession = Core::self()->activeSession(); if (!activeSession) { return; } QList openProjects; openProjects.reserve( m_projects.size() ); for (IProject* project : qAsConst(m_projects)) { openProjects.append(project->projectFile().toUrl()); } activeSession->setContainedProjects( openProjects ); } // Recursively collects builder dependencies for a project. static void collectBuilders( QList< IProjectBuilder* >& destination, IProjectBuilder* topBuilder, IProject* project ) { const QList auxBuilders = topBuilder->additionalBuilderPlugins(project); destination.append( auxBuilders ); for (IProjectBuilder* auxBuilder : auxBuilders ) { collectBuilders( destination, auxBuilder, project ); } } QVector findPluginsForProject( IProject* project ) const { const QList plugins = m_core->pluginController()->loadedPlugins(); const IBuildSystemManager* const buildSystemManager = project->buildSystemManager(); QVector projectPlugins; QList buildersForKcm; // Important to also include the "top" builder for the project, so // projects with only one such builder are kept working. Otherwise the project config // dialog is empty for such cases. if (buildSystemManager) { buildersForKcm << buildSystemManager->builder(); collectBuilders( buildersForKcm, buildSystemManager->builder(), project ); } for (auto plugin : plugins) { auto info = m_core->pluginController()->pluginInfo(plugin); auto* manager = plugin->extension(); if( manager && manager != project->projectFileManager() ) { // current plugin is a manager but does not apply to given project, skip continue; } auto* builder = plugin->extension(); if ( builder && !buildersForKcm.contains( builder ) ) { continue; } // Do not show config pages for analyzer tools which need a buildSystemManager // TODO: turn into generic feature to disable plugin config pages which do not apply for a project if (!buildSystemManager) { const auto required = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-IRequired")); if (required.contains(QLatin1String("org.kdevelop.IBuildSystemManager"))) { continue; } } qCDebug(SHELL) << "Using plugin" << info.pluginId() << "for project" << project->name(); projectPlugins << plugin; } return projectPlugins; } void updateActionStates() { // if only one project loaded, this is always our target int itemCount = (m_projects.size() == 1) ? 1 : 0; if (itemCount == 0) { // otherwise base on selection auto* itemContext = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (itemContext) { itemCount = itemContext->items().count(); } } m_openConfig->setEnabled(itemCount == 1); m_closeProject->setEnabled(itemCount > 0); } QSet selectedProjects() { QSet projects; // if only one project loaded, this is our target if (m_projects.count() == 1) { projects.insert(m_projects.at(0)); } else { // otherwise base on selection auto* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (ctx) { const auto items = ctx->items(); for (ProjectBaseItem* item : items) { projects.insert(item->project()); } } } return projects; } void openProjectConfig() { auto projects = selectedProjects(); if (projects.count() == 1) { q->configureProject(*projects.constBegin()); } } void closeSelectedProjects() { const auto projects = selectedProjects(); for (IProject* project : projects) { q->closeProject(project); } } void importProject(const QUrl& url_) { QUrl url(url_); if (url.isLocalFile()) { const QString path = QFileInfo(url.toLocalFile()).canonicalFilePath(); if (!path.isEmpty()) { url = QUrl::fromLocalFile(path); } } if ( !url.isValid() ) { - KMessageBox::error(Core::self()->uiControllerInternal()->activeMainWindow(), - i18n("Invalid Location: %1", url.toDisplayString(QUrl::PreferLocalFile))); + const QString messageText = i18n("Invalid Location: %1", url.toDisplayString(QUrl::PreferLocalFile)); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return; } if ( m_currentlyOpening.contains(url)) { qCDebug(SHELL) << "Already opening " << url << ". Aborting."; - KPassivePopup::message( i18n( "Project already being opened"), - i18n( "Already opening %1, not opening again", - url.toDisplayString(QUrl::PreferLocalFile) ), - m_core->uiController()->activeMainWindow() ); + const QString messageText = + i18n("Already opening %1, not opening again", url.toDisplayString(QUrl::PreferLocalFile)); + auto* message = new Sublime::Message(messageText, Sublime::Message::Information); + message->setAutoHide(0); + ICore::self()->uiController()->postMessage(message); return; } const auto projects = m_projects; for (IProject* project : projects) { if ( url == project->projectFile().toUrl() ) { if ( dialog->userWantsReopen() ) { // close first, then open again by falling through q->closeProject(project); } else { // abort return; } } } m_currentlyOpening += url; m_core->pluginControllerInternal()->loadProjectPlugins(); auto* project = new Project(); QObject::connect(project, &Project::aboutToOpen, q, &ProjectController::projectAboutToBeOpened); if ( !project->open( Path(url) ) ) { m_currentlyOpening.removeAll(url); q->abortOpeningProject(project); project->deleteLater(); } } void areaChanged(Sublime::Area* area) { KActionCollection* ac = m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); ac->action(QStringLiteral("commit_current_project"))->setEnabled(area->objectName() == QLatin1String("code")); ac->action(QStringLiteral("commit_current_project"))->setVisible(area->objectName() == QLatin1String("code")); } }; IProjectDialogProvider::IProjectDialogProvider() {} IProjectDialogProvider::~IProjectDialogProvider() {} ProjectDialogProvider::ProjectDialogProvider(ProjectControllerPrivate* p) : d(p) {} ProjectDialogProvider::~ProjectDialogProvider() {} bool writeNewProjectFile( const QString& localConfigFile, const QString& name, const QString& createdFrom, const QString& manager ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( localConfigFile, KConfig::SimpleConfig ); if (!cfg->isConfigWritable(true)) { qCDebug(SHELL) << "can't write to configfile"; return false; } KConfigGroup grp = cfg->group( "Project" ); grp.writeEntry( "Name", name ); grp.writeEntry( "CreatedFrom", createdFrom ); grp.writeEntry( "Manager", manager ); cfg->sync(); return true; } bool writeProjectSettingsToConfigFile(const QUrl& projectFileUrl, OpenProjectDialog* dlg) { if ( !projectFileUrl.isLocalFile() ) { QTemporaryFile tmp; if ( !tmp.open() ) { return false; } if ( !writeNewProjectFile( tmp.fileName(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ) ) { return false; } // explicitly close file before uploading it, see also: https://bugs.kde.org/show_bug.cgi?id=254519 tmp.close(); auto uploadJob = KIO::file_copy(QUrl::fromLocalFile(tmp.fileName()), projectFileUrl); KJobWidgets::setWindow(uploadJob, Core::self()->uiControllerInternal()->defaultMainWindow()); return uploadJob->exec(); } // Here and above we take .filename() part of the selectedUrl() to make it relative to the project root, // thus keeping .kdev file relocatable return writeNewProjectFile( projectFileUrl.toLocalFile(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ); } bool projectFileExists( const QUrl& u ) { if( u.isLocalFile() ) { return QFileInfo::exists( u.toLocalFile() ); } else { auto statJob = KIO::stat(u, KIO::StatJob::DestinationSide, 0, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, Core::self()->uiControllerInternal()->activeMainWindow()); return statJob->exec(); } } bool equalProjectFile( const QString& configPath, OpenProjectDialog* dlg ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( configPath, KConfig::SimpleConfig ); KConfigGroup grp = cfg->group( "Project" ); QString defaultName = dlg->projectFileUrl().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).fileName(); return (grp.readEntry( "Name", QString() ) == dlg->projectName() || dlg->projectName() == defaultName) && grp.readEntry( "Manager", QString() ) == dlg->projectManager(); } QUrl ProjectDialogProvider::askProjectConfigLocation(bool fetch, const QUrl& startUrl, const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin) { Q_ASSERT(d); ScopedDialog dlg(fetch, startUrl, repoUrl, vcsOrProviderPlugin, Core::self()->uiController()->activeMainWindow()); if(dlg->exec() == QDialog::Rejected) { return QUrl(); } QUrl projectFileUrl = dlg->projectFileUrl(); qCDebug(SHELL) << "selected project:" << projectFileUrl << dlg->projectName() << dlg->projectManager(); if ( dlg->projectManager() == QLatin1String("") ) { return projectFileUrl; } // controls if existing project file should be saved bool writeProjectConfigToFile = true; if( projectFileExists( projectFileUrl ) ) { // check whether config is equal bool shouldAsk = true; if( projectFileUrl == dlg->selectedUrl() ) { if( projectFileUrl.isLocalFile() ) { shouldAsk = !equalProjectFile( projectFileUrl.toLocalFile(), dlg ); } else { shouldAsk = false; QTemporaryFile tmpFile; if (tmpFile.open()) { auto downloadJob = KIO::file_copy(projectFileUrl, QUrl::fromLocalFile(tmpFile.fileName())); KJobWidgets::setWindow(downloadJob, qApp->activeWindow()); if (downloadJob->exec()) { shouldAsk = !equalProjectFile(tmpFile.fileName(), dlg); } } } } if ( shouldAsk ) { KGuiItem yes = KStandardGuiItem::yes(); yes.setText(i18n("Override")); yes.setToolTip(i18nc("@info:tooltip", "Continue to open the project and use the just provided project configuration.")); yes.setIcon(QIcon()); KGuiItem no = KStandardGuiItem::no(); no.setText(i18n("Open Existing File")); no.setToolTip(i18nc("@info:tooltip", "Continue to open the project but use the existing project configuration.")); no.setIcon(QIcon()); KGuiItem cancel = KStandardGuiItem::cancel(); cancel.setToolTip(i18nc("@info:tooltip", "Cancel and do not open the project.")); int ret = KMessageBox::questionYesNoCancel(qApp->activeWindow(), i18n("There already exists a project configuration file at %1.\n" "Do you want to override it or open the existing file?", projectFileUrl.toDisplayString(QUrl::PreferLocalFile)), i18n("Override existing project configuration"), yes, no, cancel ); if ( ret == KMessageBox::No ) { writeProjectConfigToFile = false; } else if ( ret == KMessageBox::Cancel ) { return QUrl(); } // else fall through and write new file } else { writeProjectConfigToFile = false; } } if (writeProjectConfigToFile) { Path projectConfigDir(projectFileUrl); projectConfigDir.setLastPathSegment(QStringLiteral(".kdev4")); auto delJob = KIO::del(projectConfigDir.toUrl()); delJob->exec(); if (!writeProjectSettingsToConfigFile(projectFileUrl, dlg)) { - KMessageBox::error(d->m_core->uiControllerInternal()->defaultMainWindow(), - i18n("Unable to create configuration file %1", projectFileUrl.url())); + const QString messageText = i18n("Unable to create configuration file %1", projectFileUrl.url()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return QUrl(); } } return projectFileUrl; } bool ProjectDialogProvider::userWantsReopen() { Q_ASSERT(d); return (KMessageBox::questionYesNo( d->m_core->uiControllerInternal()->defaultMainWindow(), i18n( "Reopen the current project?" ) ) == KMessageBox::No) ? false : true; } void ProjectController::setDialogProvider(IProjectDialogProvider* dialog) { Q_D(ProjectController); Q_ASSERT(d->dialog); delete d->dialog; d->dialog = dialog; } ProjectController::ProjectController( Core* core ) : IProjectController(core) , d_ptr(new ProjectControllerPrivate(core, this)) { qRegisterMetaType>(); setObjectName(QStringLiteral("ProjectController")); //NOTE: this is required to be called here, such that the // actions are available when the UI controller gets // initialized *before* the project controller if (Core::self()->setupFlags() != Core::NoUi) { setupActions(); } } void ProjectController::setupActions() { Q_D(ProjectController); KActionCollection * ac = d->m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); QAction*action; d->m_openProject = action = ac->addAction( QStringLiteral("project_open") ); action->setText(i18nc( "@action", "Open / Import Project..." ) ); action->setToolTip( i18nc( "@info:tooltip", "Open or import project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Open an existing KDevelop 4 project or import " "an existing Project into KDevelop 4. This entry " "allows one to select a KDevelop4 project file " "or an existing directory to open it in KDevelop. " "When opening an existing directory that does " "not yet have a KDevelop4 project file, the file " "will be created." ) ); action->setIcon(QIcon::fromTheme(QStringLiteral("project-open"))); connect(action, &QAction::triggered, this, [&] { openProject(); }); d->m_fetchProject = action = ac->addAction( QStringLiteral("project_fetch") ); action->setText(i18nc( "@action", "Fetch Project..." ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("edit-download") ) ); action->setToolTip( i18nc( "@info:tooltip", "Fetch project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Guides the user through the project fetch " "and then imports it into KDevelop 4." ) ); // action->setIcon(QIcon::fromTheme("project-open")); connect( action, &QAction::triggered, this, &ProjectController::fetchProject ); // action = ac->addAction( "project_close" ); // action->setText( i18n( "C&lose Project" ) ); // connect( action, SIGNAL(triggered(bool)), SLOT(closeProject()) ); // action->setToolTip( i18n( "Close project" ) ); // action->setWhatsThis( i18n( "Closes the current project." ) ); // action->setEnabled( false ); d->m_closeProject = action = ac->addAction( QStringLiteral("project_close") ); connect(action, &QAction::triggered, this, [this] { Q_D(ProjectController); d->closeSelectedProjects(); } ); action->setText( i18nc( "@action", "Close Project(s)" ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("project-development-close") ) ); action->setToolTip( i18nc( "@info:tooltip", "Closes all currently selected projects" ) ); action->setEnabled( false ); d->m_openConfig = action = ac->addAction( QStringLiteral("project_open_config") ); connect(action, &QAction::triggered, this, [this] { Q_D(ProjectController); d->openProjectConfig(); } ); action->setText( i18n( "Open Configuration..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("configure")) ); action->setEnabled( false ); action = ac->addAction( QStringLiteral("commit_current_project") ); connect( action, &QAction::triggered, this, &ProjectController::commitCurrentProject ); action->setText( i18n( "Commit Current Project..." ) ); action->setIconText( i18n( "Commit..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("svn-commit")) ); connect(d->m_core->uiControllerInternal()->defaultMainWindow(), &MainWindow::areaChanged, this, [this] (Sublime::Area* area) { Q_D(ProjectController); d->areaChanged(area); }); d->m_core->uiControllerInternal()->area(0, QStringLiteral("code"))->addAction(action); KSharedConfig * config = KSharedConfig::openConfig().data(); // KConfigGroup group = config->group( "General Options" ); d->m_recentProjectsAction = KStandardAction::openRecent(this, SLOT(openProject(QUrl)), this); ac->addAction( QStringLiteral("project_open_recent"), d->m_recentProjectsAction ); d->m_recentProjectsAction->setText( i18n( "Open Recent Project" ) ); d->m_recentProjectsAction->setWhatsThis( i18nc( "@info:whatsthis", "Opens recently opened project." ) ); d->m_recentProjectsAction->loadEntries( KConfigGroup(config, "RecentProjects") ); auto* openProjectForFileAction = new QAction( this ); ac->addAction(QStringLiteral("project_open_for_file"), openProjectForFileAction); openProjectForFileAction->setText(i18n("Open Project for Current File")); openProjectForFileAction->setIcon(QIcon::fromTheme(QStringLiteral("project-open"))); connect( openProjectForFileAction, &QAction::triggered, this, &ProjectController::openProjectForUrlSlot); } ProjectController::~ProjectController() { Q_D(ProjectController); delete d->model; delete d->dialog; } void ProjectController::cleanup() { Q_D(ProjectController); if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } saveRecentProjectsActionEntries(); d->m_cleaningUp = true; if( buildSetModel() ) { buildSetModel()->storeToSession( Core::self()->activeSession() ); } closeAllProjects(); } void ProjectController::saveRecentProjectsActionEntries() { Q_D(ProjectController); if (!d->m_recentProjectsAction) return; auto config = KSharedConfig::openConfig(); KConfigGroup recentGroup = config->group("RecentProjects"); d->m_recentProjectsAction->saveEntries( recentGroup ); config->sync(); } void ProjectController::initialize() { Q_D(ProjectController); d->buildset = new ProjectBuildSetModel( this ); buildSetModel()->loadFromSession( Core::self()->activeSession() ); connect( this, &ProjectController::projectOpened, d->buildset, &ProjectBuildSetModel::loadFromProject ); connect( this, &ProjectController::projectClosing, d->buildset, &ProjectBuildSetModel::saveToProject ); connect( this, &ProjectController::projectClosed, d->buildset, &ProjectBuildSetModel::projectClosed ); d->m_changesModel = new ProjectChangesModel(this); loadSettings(false); d->dialog = new ProjectDialogProvider(d); QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/ProjectController"), this, QDBusConnection::ExportScriptableSlots ); KSharedConfigPtr config = Core::self()->activeSession()->config(); KConfigGroup group = config->group( "General Options" ); const auto projects = group.readEntry( "Open Projects", QList() ); connect( Core::self()->selectionController(), &ISelectionController::selectionChanged, this, [this]() { Q_D(ProjectController); d->updateActionStates(); } ); connect(this, &ProjectController::projectOpened, this, [this]() { Q_D(ProjectController); d->updateActionStates(); }); connect(this, &ProjectController::projectClosing, this, [this]() { Q_D(ProjectController); d->updateActionStates(); }); QTimer::singleShot(0, this, [this, projects](){ openProjects(projects); emit initialized(); }); } void ProjectController::openProjects(const QList& projects) { for (const QUrl& url : projects) { openProject(url); } } void ProjectController::loadSettings( bool projectIsLoaded ) { Q_UNUSED(projectIsLoaded) } void ProjectController::saveSettings( bool projectIsLoaded ) { Q_UNUSED( projectIsLoaded ); } int ProjectController::projectCount() const { Q_D(const ProjectController); return d->m_projects.count(); } IProject* ProjectController::projectAt( int num ) const { Q_D(const ProjectController); if( !d->m_projects.isEmpty() && num >= 0 && num < d->m_projects.count() ) return d->m_projects.at( num ); return nullptr; } QList ProjectController::projects() const { Q_D(const ProjectController); return d->m_projects; } void ProjectController::eventuallyOpenProjectFile(KIO::Job* _job, const KIO::UDSEntryList& entries) { Q_D(ProjectController); auto* job = qobject_cast(_job); Q_ASSERT(job); for (const KIO::UDSEntry& entry : entries) { if(d->m_foundProjectFile) break; if(!entry.isDir()) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if(name.endsWith(QLatin1String(".kdev4"))) { //We have found a project-file, open it openProject(Path(Path(job->url()), name).toUrl()); d->m_foundProjectFile = true; } } } } void ProjectController::openProjectForUrlSlot(bool) { if(ICore::self()->documentController()->activeDocument()) { QUrl url = ICore::self()->documentController()->activeDocument()->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(!project) { openProjectForUrl(url); }else{ - KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("Project already open: %1", project->name())); + auto* message = new Sublime::Message(i18n("Project already open: %1", project->name()), Sublime::Message::Error); + Core::self()->uiController()->postMessage(message); } }else{ - KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("No active document")); + auto* message = new Sublime::Message(i18n("No active document"), Sublime::Message::Error); + Core::self()->uiController()->postMessage(message); } } void ProjectController::openProjectForUrl(const QUrl& sourceUrl) { Q_D(ProjectController); Q_ASSERT(!sourceUrl.isRelative()); QUrl dirUrl = sourceUrl; if (sourceUrl.isLocalFile() && !QFileInfo(sourceUrl.toLocalFile()).isDir()) { dirUrl = dirUrl.adjusted(QUrl::RemoveFilename); } QUrl testAt = dirUrl; d->m_foundProjectFile = false; while(!testAt.path().isEmpty()) { KIO::ListJob* job = KIO::listDir(testAt); connect(job, &KIO::ListJob::entries, this, &ProjectController::eventuallyOpenProjectFile); KJobWidgets::setWindow(job, ICore::self()->uiController()->activeMainWindow()); job->exec(); if(d->m_foundProjectFile) { //Fine! We have directly opened the project d->m_foundProjectFile = false; return; } QUrl oldTest = testAt.adjusted(QUrl::RemoveFilename); if(oldTest == testAt) break; } QUrl askForOpen = d->dialog->askProjectConfigLocation(false, dirUrl); if(askForOpen.isValid()) openProject(askForOpen); } void ProjectController::openProject( const QUrl &projectFile ) { Q_D(ProjectController); QUrl url = projectFile; if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); if ( url.isEmpty() ) { return; } } Q_ASSERT(!url.isRelative()); QList existingSessions; if(!Core::self()->sessionController()->activeSession()->containedProjects().contains(url)) { const auto sessions = Core::self()->sessionController()->sessions(); for (const Session* session : sessions) { if(session->containedProjects().contains(url)) { existingSessions << session; #if 0 ///@todo Think about this! Problem: The session might already contain files, the debugger might be active, etc. //If this session is empty, close it if(Core::self()->sessionController()->activeSession()->description().isEmpty()) { //Terminate this instance of kdevelop if the user agrees const auto windows = Core::self()->uiController()->controller()->mainWindows(); for (Sublime::MainWindow* window : windows) { window->close(); } } #endif } } } if ( ! existingSessions.isEmpty() ) { ScopedDialog dialog(Core::self()->uiControllerInternal()->activeMainWindow()); dialog->setWindowTitle(i18n("Project Already Open")); auto mainLayout = new QVBoxLayout(dialog); mainLayout->addWidget(new QLabel(i18n("The project you're trying to open is already open in at least one " "other session.
What do you want to do?"))); QGroupBox sessions; sessions.setLayout(new QVBoxLayout); QRadioButton* newSession = new QRadioButton(i18n("Add project to current session")); sessions.layout()->addWidget(newSession); newSession->setChecked(true); for (const Session* session : qAsConst(existingSessions)) { QRadioButton* button = new QRadioButton(i18n("Open session %1", session->description())); button->setProperty("sessionid", session->id().toString()); sessions.layout()->addWidget(button); } sessions.layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding)); mainLayout->addWidget(&sessions); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Abort); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); mainLayout->addWidget(buttonBox); if (!dialog->exec()) return; for (const QObject* obj : sessions.children()) { if ( const auto* button = qobject_cast(obj) ) { QString sessionid = button->property("sessionid").toString(); if ( button->isChecked() && ! sessionid.isEmpty() ) { Core::self()->sessionController()->loadSession(sessionid); return; } } } } if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); } if ( !url.isEmpty() ) { d->importProject(url); } } bool ProjectController::fetchProjectFromUrl(const QUrl& repoUrl, FetchFlags fetchFlags) { Q_D(ProjectController); IPlugin* vcsOrProviderPlugin = nullptr; // TODO: query also projectprovider plugins, and that before plain vcs plugins // e.g. KDE provider plugin could catch URLs from mirror or pickup kde:repo things auto* pluginController = d->m_core->pluginController(); const auto& vcsPlugins = pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IBasicVersionControl")); for (auto* plugin : vcsPlugins) { auto* iface = plugin->extension(); if (iface->isValidRemoteRepositoryUrl(repoUrl)) { vcsOrProviderPlugin = plugin; break; } } if (!vcsOrProviderPlugin) { if (fetchFlags.testFlag(FetchShowErrorIfNotSupported)) { - KMessageBox::error(Core::self()->uiController()->activeMainWindow(), - i18n("No enabled plugin supports this repository URL: %1", repoUrl.toDisplayString())); + const QString messageText = + i18n("No enabled plugin supports this repository URL: %1", repoUrl.toDisplayString()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); } return false; } const QUrl url = d->dialog->askProjectConfigLocation(true, QUrl(), repoUrl, vcsOrProviderPlugin); if (!url.isEmpty()) { d->importProject(url); } return true; } void ProjectController::fetchProject() { Q_D(ProjectController); QUrl url = d->dialog->askProjectConfigLocation(true); if ( !url.isEmpty() ) { d->importProject(url); } } void ProjectController::projectImportingFinished( IProject* project ) { Q_D(ProjectController); if( !project ) { qCWarning(SHELL) << "OOOPS: 0-pointer project"; return; } IPlugin *managerPlugin = project->managerPlugin(); QList pluglist; pluglist.append( managerPlugin ); d->m_projectPlugins.insert( project, pluglist ); d->m_projects.append( project ); if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } if (Core::self()->setupFlags() != Core::NoUi) { d->m_recentProjectsAction->addUrl( project->projectFile().toUrl() ); saveRecentProjectsActionEntries(); } Q_ASSERT(d->m_currentlyOpening.contains(project->projectFile().toUrl())); d->m_currentlyOpening.removeAll(project->projectFile().toUrl()); emit projectOpened( project ); reparseProject(project); } // helper method for closeProject() void ProjectController::unloadUnusedProjectPlugins(IProject* proj) { Q_D(ProjectController); QList pluginsForProj = d->m_projectPlugins.value( proj ); d->m_projectPlugins.remove( proj ); QList otherProjectPlugins; for (const QList& _list : qAsConst(d->m_projectPlugins)) { otherProjectPlugins << _list; } QSet pluginsForProjSet = QSet::fromList( pluginsForProj ); QSet otherPrjPluginsSet = QSet::fromList( otherProjectPlugins ); // loaded - target = tobe unloaded. const QSet tobeRemoved = pluginsForProjSet.subtract( otherPrjPluginsSet ); for (IPlugin* _plugin : tobeRemoved) { KPluginMetaData _plugInfo = Core::self()->pluginController()->pluginInfo( _plugin ); if( _plugInfo.isValid() ) { QString _plugName = _plugInfo.pluginId(); qCDebug(SHELL) << "about to unloading :" << _plugName; Core::self()->pluginController()->unloadPlugin( _plugName ); } } } // helper method for closeProject() void ProjectController::closeAllOpenedFiles(IProject* proj) { const auto documents = Core::self()->documentController()->openDocuments(); for (IDocument* doc : documents) { if (proj->inProject(IndexedString(doc->url()))) { doc->close(); } } } // helper method for closeProject() void ProjectController::initializePluginCleanup(IProject* proj) { // Unloading (and thus deleting) these plugins is not a good idea just yet // as we're being called by the view part and it gets deleted when we unload the plugin(s) // TODO: find a better place to unload connect(proj, &IProject::destroyed, this, [this] { Q_D(ProjectController); d->unloadAllProjectPlugins(); }); } void ProjectController::takeProject(IProject* proj) { Q_D(ProjectController); if (!proj) { return; } // loading might have failed d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); d->m_projects.removeAll(proj); emit projectClosing(proj); //Core::self()->saveSettings(); // The project file is being closed. // Now we can save settings for all of the Core // objects including this one!! unloadUnusedProjectPlugins(proj); closeAllOpenedFiles(proj); proj->close(); if (d->m_projects.isEmpty()) { initializePluginCleanup(proj); } if(!d->m_cleaningUp) d->saveListOfOpenedProjects(); emit projectClosed(proj); } void ProjectController::closeProject(IProject* proj) { takeProject(proj); proj->deleteLater(); // be safe when deleting } void ProjectController::closeAllProjects() { Q_D(ProjectController); const auto projects = d->m_projects; for (auto* project : projects) { closeProject(project); } } void ProjectController::abortOpeningProject(IProject* proj) { Q_D(ProjectController); d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); emit projectOpeningAborted(proj); } ProjectModel* ProjectController::projectModel() { Q_D(ProjectController); return d->model; } IProject* ProjectController::findProjectForUrl( const QUrl& url ) const { Q_D(const ProjectController); if (d->m_projects.isEmpty()) { return nullptr; } ProjectBaseItem* item = d->model->itemForPath(IndexedString(url)); if (item) { return item->project(); } return nullptr; } IProject* ProjectController::findProjectByName( const QString& name ) { Q_D(ProjectController); auto it = std::find_if(d->m_projects.constBegin(), d->m_projects.constEnd(), [&](IProject* proj) { return (proj->name() == name); }); return (it != d->m_projects.constEnd()) ? *it : nullptr; } void ProjectController::configureProject( IProject* project ) { Q_D(ProjectController); d->projectConfig( project ); } void ProjectController::addProject(IProject* project) { Q_D(ProjectController); Q_ASSERT(project); if (d->m_projects.contains(project)) { qCWarning(SHELL) << "Project already tracked by this project controller:" << project; return; } // fake-emit signals so listeners are aware of a new project being added emit projectAboutToBeOpened(project); project->setParent(this); d->m_projects.append(project); emit projectOpened(project); } bool ProjectController::isProjectNameUsed( const QString& name ) const { const auto projects = this->projects(); return std::any_of(projects.begin(), projects.end(), [&](IProject* p) { return (p->name() == name); }); } QUrl ProjectController::projectsBaseDirectory() const { KConfigGroup group = ICore::self()->activeSession()->config()->group( "Project Manager" ); return group.readEntry("Projects Base Directory", QUrl::fromLocalFile(QDir::homePath() + QLatin1String("/projects"))); } QString ProjectController::prettyFilePath(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(!project) { // Find a project with the correct base directory at least const auto projects = Core::self()->projectController()->projects(); auto it = std::find_if(projects.begin(), projects.end(), [&](IProject* candidateProject) { return (candidateProject->path().toUrl().isParentOf(url)); }); if (it != projects.end()) { project = *it; } } Path parent = Path(url).parent(); QString prefixText; if (project) { if (format == FormatHtml) { prefixText = QLatin1String("") + project->name() + QLatin1String("/"); } else { prefixText = project->name() + QLatin1Char(':'); } QString relativePath = project->path().relativePath(parent); if(relativePath.startsWith(QLatin1String("./"))) { relativePath.remove(0, 2); } if (!relativePath.isEmpty()) { prefixText += relativePath + QLatin1Char('/'); } } else { prefixText = parent.pathOrUrl() + QLatin1Char('/'); } return prefixText; } QString ProjectController::prettyFileName(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(project && project->path() == Path(url)) { if (format == FormatHtml) { return QLatin1String("") + project->name() + QLatin1String(""); } else { return project->name(); } } QString prefixText = prettyFilePath( url, format ); if (format == FormatHtml) { return prefixText + QLatin1String("") + url.fileName() + QLatin1String(""); } else { return prefixText + url.fileName(); } } ContextMenuExtension ProjectController::contextMenuExtension(Context* ctx, QWidget* parent) { Q_D(ProjectController); Q_UNUSED(parent); ContextMenuExtension ext; if ( ctx->type() != Context::ProjectItemContext) { return ext; } if (!static_cast(ctx)->items().isEmpty() ) { auto* action = new QAction(i18n("Reparse the Entire Project"), this); connect(action, &QAction::triggered, this, [this] { Q_D(ProjectController); const auto projects = d->selectedProjects(); for (auto* project : projects) { reparseProject(project, true, true); } }); ext.addAction(ContextMenuExtension::ProjectGroup, action); return ext; } ext.addAction(ContextMenuExtension::ProjectGroup, d->m_openProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_fetchProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_recentProjectsAction); return ext; } ProjectBuildSetModel* ProjectController::buildSetModel() { Q_D(ProjectController); return d->buildset; } ProjectChangesModel* ProjectController::changesModel() { Q_D(ProjectController); return d->m_changesModel; } void ProjectController::commitCurrentProject() { IDocument* doc=ICore::self()->documentController()->activeDocument(); if(!doc) return; QUrl url=doc->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(project && project->versionControlPlugin()) { IPlugin* plugin = project->versionControlPlugin(); auto* vcs=plugin->extension(); if(vcs) { ICore::self()->documentController()->saveAllDocuments(KDevelop::IDocument::Silent); const Path basePath = project->path(); VCSCommitDiffPatchSource* patchSource = new VCSCommitDiffPatchSource(new VCSStandardDiffUpdater(vcs, basePath.toUrl())); bool ret = showVcsDiff(patchSource); if(!ret) { ScopedDialog commitDialog(patchSource); commitDialog->setCommitCandidates(patchSource->infos()); commitDialog->exec(); } } } } QString ProjectController::mapSourceBuild( const QString& path_, bool reverse, bool fallbackRoot ) const { Q_D(const ProjectController); Path path(path_); IProject* sourceDirProject = nullptr, *buildDirProject = nullptr; for (IProject* proj : qAsConst(d->m_projects)) { if(proj->path().isParentOf(path) || proj->path() == path) sourceDirProject = proj; if(proj->buildSystemManager()) { Path buildDir = proj->buildSystemManager()->buildDirectory(proj->projectItem()); if(buildDir.isValid() && (buildDir.isParentOf(path) || buildDir == path)) buildDirProject = proj; } } if(!reverse) { // Map-target is the build directory if(sourceDirProject && sourceDirProject->buildSystemManager()) { // We're in the source, map into the build directory QString relativePath = sourceDirProject->path().relativePath(path); Path build = sourceDirProject->buildSystemManager()->buildDirectory(sourceDirProject->projectItem()); build.addPath(relativePath); while(!QFile::exists(build.path())) build = build.parent(); return build.pathOrUrl(); }else if(buildDirProject && fallbackRoot) { // We're in the build directory, map to the build directory root return buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()).pathOrUrl(); } }else{ // Map-target is the source directory if(buildDirProject) { Path build = buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()); // We're in the source, map into the build directory QString relativePath = build.relativePath(path); Path source = buildDirProject->path(); source.addPath(relativePath); while(!QFile::exists(source.path())) source = source.parent(); return source.pathOrUrl(); }else if(sourceDirProject && fallbackRoot) { // We're in the source directory, map to the root return sourceDirProject->path().pathOrUrl(); } } return QString(); } void KDevelop::ProjectController::reparseProject(IProject *project, bool forceUpdate, bool forceAll) { Q_D(ProjectController); if (auto job = d->m_parseJobs.value(project)) { job->kill(); } d->m_parseJobs[project] = new KDevelop::ParseProjectJob(project, forceUpdate, forceAll); ICore::self()->runController()->registerJob(d->m_parseJobs[project]); } } diff --git a/kdevplatform/shell/runcontroller.cpp b/kdevplatform/shell/runcontroller.cpp index 64c1e1b0ed..6b80bf5c31 100644 --- a/kdevplatform/shell/runcontroller.cpp +++ b/kdevplatform/shell/runcontroller.cpp @@ -1,1103 +1,1093 @@ /* 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 "core.h" #include "uicontroller.h" #include "projectcontroller.h" #include "mainwindow.h" #include "launchconfiguration.h" #include "launchconfigurationdialog.h" #include "unitylauncher.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"); } } } using Target = QPair; 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("debug-run")); } 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 KDevelop::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; QMap > launchAsInfo; KDevelop::ProjectBaseItem* contextItem; DebugMode* debugMode; ExecuteMode* executeMode; ProfileMode* profileMode; UnityLauncher* unityLauncher; 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() : QString() ); grp.writeEntry( Strings::CurrentLaunchConfigNameEntry(), l->configGroupName() ); grp.sync(); } } 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 ) { const auto launchers = type->launchers(); auto it = std::find_if(launchers.begin(), launchers.end(), [&](ILauncher* l) { //qCDebug(SHELL) << "available launcher" << l << l->id() << l->supportedModes(); return (l->supportedModes().contains(mode->id())); }); if (it != launchers.end()) { ILauncher* launcher = *it; QStringList itemPath = Core::self()->projectController()->projectModel()->pathFromIndex(contextItem->index()); auto it = std::find_if(launchConfigurations.constBegin(), launchConfigurations.constEnd(), [&] (LaunchConfiguration* l) { QStringList path = l->config().readEntry(Strings::ConfiguredFromProjectItemEntry(), QStringList()); if (l->type() == type && path == itemPath) { qCDebug(SHELL) << "already generated ilaunch" << path; return true; } return false; }); ILaunchConfiguration* ilaunch = (it != launchConfigurations.constEnd()) ? *it : nullptr; if (!ilaunch) { ilaunch = q->createLaunchConfiguration( type, qMakePair( mode->id(), launcher->id() ), contextItem->project(), contextItem->text() ); auto* launch = static_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 = nullptr; 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 ) ) { const auto actions = currentTargetAction->actions(); for (QAction* a : 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( const KSharedConfigPtr& cfg, IProject* prj ) { KConfigGroup group(cfg, Strings::LaunchConfigurationsGroup()); const QStringList configs = group.readEntry(Strings::LaunchConfigurationsListEntry(), QStringList()); for (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 { qCWarning(SHELL) << "couldn't find type for id:" << id << ". Known types:" << launchConfigurationTypes.keys(); } return nullptr; } }; RunController::RunController(QObject *parent) : IRunController(parent) , d_ptr(new RunControllerPrivate) { Q_D(RunController); 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 = nullptr; d->state = Idle; d->q = this; d->delegate = new RunDelegate(this); d->contextItem = nullptr; d->executeMode = nullptr; d->debugMode = nullptr; d->profileMode = nullptr; d->unityLauncher = new UnityLauncher(this); d->unityLauncher->setLauncherId(KAboutData::applicationData().desktopFileName()); if(!(Core::self()->setupFlags() & Core::NoUi)) { // Note that things like registerJob() do not work without the actions, it'll simply crash. setupActions(); } } RunController::~RunController() = default; void KDevelop::RunController::launchChanged( LaunchConfiguration* l ) { Q_D(RunController); const auto actions = d->currentTargetAction->actions(); for (QAction* a : actions) { if( static_cast( a->data().value() ) == l ) { a->setText( d->launchActionText( l ) ); break; } } } void RunController::cleanup() { Q_D(RunController); delete d->executeMode; d->executeMode = nullptr; delete d->profileMode; d->profileMode = nullptr; delete d->debugMode; d->debugMode = nullptr; stopAllProcesses(); d->saveCurrentLaunchAction(); } void RunController::initialize() { Q_D(RunController); 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(), nullptr ); const auto projects = Core::self()->projectController()->projects(); for (IProject* project : 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 nullptr; } auto* run = static_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), - QString()); + const QString messageText = i18n("The current launch configuration does not support the '%1' mode.", runMode); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return nullptr; } KJob* launchJob = launcher->start(runMode, run); registerJob(launchJob); return launchJob; } void RunController::setupActions() { Q_D(RunController); 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, &RunController::showConfigurationDialog); 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::ALT + 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 that is 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 ) { Q_D(RunController); return d->launchConfigurationTypeForId( id ); } void KDevelop::RunController::slotProjectOpened(KDevelop::IProject * project) { Q_D(RunController); d->readLaunchConfigs( project->projectConfiguration(), project ); d->updateCurrentLaunchAction(); } void KDevelop::RunController::slotProjectClosing(KDevelop::IProject * project) { Q_D(RunController); if (!d->currentTargetAction) return; const auto actions = d->currentTargetAction->actions(); for (QAction* action : 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() { Q_D(RunController); if (d->launchConfigurations.isEmpty()) { showConfigurationDialog(); } if (!d->launchConfigurations.isEmpty()) { executeDefaultLaunch( QStringLiteral("debug") ); } } void RunController::slotProfile() { Q_D(RunController); if (d->launchConfigurations.isEmpty()) { showConfigurationDialog(); } if (!d->launchConfigurations.isEmpty()) { executeDefaultLaunch( QStringLiteral("profile") ); } } void RunController::slotExecute() { Q_D(RunController); if (d->launchConfigurations.isEmpty()) { showConfigurationDialog(); } if (!d->launchConfigurations.isEmpty()) { executeDefaultLaunch( QStringLiteral("execute") ); } } void KDevelop::RunController::showConfigurationDialog() const { LaunchConfigurationDialog dlg; dlg.exec(); } LaunchConfiguration* KDevelop::RunController::defaultLaunch() const { Q_D(const RunController); QAction* projectAction = d->currentTargetAction->currentAction(); if( projectAction ) return static_cast(qvariant_cast(projectAction->data())); return nullptr; } void KDevelop::RunController::registerJob(KJob * job) { Q_D(RunController); if (!job) return; if (!(job->capabilities() & KJob::Killable)) { // see e.g. https://bugs.kde.org/show_bug.cgi?id=314187 qCWarning(SHELL) << "non-killable job" << job << "registered - this might lead to crashes on shutdown."; } if (!d->jobs.contains(job)) { QAction* stopJobAction = nullptr; if (Core::self()->setupFlags() != Core::NoUi) { stopJobAction = new QAction(job->objectName().isEmpty() ? i18n("<%1> Unnamed job", QString::fromUtf8(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 ); // FIXME percent is a private signal and thus we cannot use new connect syntax connect(job, SIGNAL(percent(KJob*,ulong)), this, SLOT(jobPercentChanged())); IRunController::registerJob(job); emit jobRegistered(job); } job->start(); checkState(); } void KDevelop::RunController::unregisterJob(KJob * job) { Q_D(RunController); 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() { Q_D(RunController); bool running = false; int jobCount = 0; int totalProgress = 0; for (auto it = d->jobs.constBegin(), end = d->jobs.constEnd(); it != end; ++it) { KJob *job = it.key(); if (!job->isSuspended()) { running = true; ++jobCount; totalProgress += job->percent(); } } d->unityLauncher->setProgressVisible(running); if (jobCount > 0) { d->unityLauncher->setProgress((totalProgress + 1) / jobCount); } else { d->unityLauncher->setProgress(0); } 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() { Q_D(RunController); // composite jobs might remove child jobs, see also: // https://bugs.kde.org/show_bug.cgi?id=258904 const auto jobs = d->jobs.keys(); for (KJob* job : jobs) { // 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 { qCWarning(SHELL) << "cannot stop non-killable job: " << job; } } } void KDevelop::RunController::slotKillJob() { auto* action = qobject_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(), nullptr, KMessageBox::NoExec); - dialog->show(); + auto* message = new Sublime::Message(job->errorString(), Sublime::Message::Error); + Core::self()->uiController()->postMessage(message); } } } void RunController::jobDestroyed(QObject* job) { Q_D(RunController); KJob* kjob = static_cast(job); if (d->jobs.contains(kjob)) { qCWarning(SHELL) << "job destroyed without emitting finished signal!"; unregisterJob(kjob); } } void RunController::jobPercentChanged() { checkState(); } 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 { Q_D(const RunController); return d->jobs.keys(); } QList RunController::launchConfigurations() const { QList configs; const auto configsInternal = launchConfigurationsInternal(); configs.reserve(configsInternal.size()); for (LaunchConfiguration* config : configsInternal) { configs << config; } return configs; } QList RunController::launchConfigurationsInternal() const { Q_D(const RunController); return d->launchConfigurations; } QList RunController::launchConfigurationTypes() const { Q_D(const RunController); return d->launchConfigurationTypes.values(); } void RunController::addConfigurationType( LaunchConfigurationType* type ) { Q_D(RunController); if( !d->launchConfigurationTypes.contains( type->id() ) ) { d->launchConfigurationTypes.insert( type->id(), type ); } } void RunController::removeConfigurationType( LaunchConfigurationType* type ) { Q_D(RunController); const auto oldLaunchConfigurations = d->launchConfigurations; for (LaunchConfiguration* l : oldLaunchConfigurations) { if( l->type() == type ) { removeLaunchConfigurationInternal( l ); } } d->launchConfigurationTypes.remove( type->id() ); } void KDevelop::RunController::addLaunchMode(KDevelop::ILaunchMode* mode) { Q_D(RunController); if( !d->launchModes.contains( mode->id() ) ) { d->launchModes.insert( mode->id(), mode ); } } QList< KDevelop::ILaunchMode* > KDevelop::RunController::launchModes() const { Q_D(const RunController); return d->launchModes.values(); } void KDevelop::RunController::removeLaunchMode(KDevelop::ILaunchMode* mode) { Q_D(RunController); d->launchModes.remove( mode->id() ); } KDevelop::ILaunchMode* KDevelop::RunController::launchModeForId(const QString& id) const { Q_D(const RunController); auto it = d->launchModes.find( id ); if( it != d->launchModes.end() ) { return it.value(); } return nullptr; } void KDevelop::RunController::addLaunchConfiguration(KDevelop::LaunchConfiguration* l) { Q_D(RunController); 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(); removeLaunchConfigurationInternal( l ); } void RunController::removeLaunchConfigurationInternal(LaunchConfiguration *l) { Q_D(RunController); const auto actions = d->currentTargetAction->actions(); for (QAction* a : actions) { if( static_cast( a->data().value() ) == l ) { bool wasSelected = a->isChecked(); d->currentTargetAction->removeAction( a ); 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) { if (auto dl = defaultLaunch()) { execute(runMode, dl); } else { qCWarning(SHELL) << "no default launch!"; } } void RunController::setDefaultLaunch(ILaunchConfiguration* l) { Q_D(RunController); const auto actions = d->currentTargetAction->actions(); for (QAction* a : actions) { if( static_cast( a->data().value() ) == l ) { a->setChecked(true); break; } } } bool launcherNameExists(const QString& name) { const auto configs = Core::self()->runControllerInternal()->launchConfigurations(); return std::any_of(configs.begin(), configs.end(), [&](ILaunchConfiguration* config) { return (config->name() == name); }); } 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(); auto* l = new LaunchConfiguration( launchConfigGroup, project ); l->setLauncherForMode( launcher.first, launcher.second ); Core::self()->runControllerInternal()->addLaunchConfiguration( l ); return l; } QItemDelegate * KDevelop::RunController::delegate() const { Q_D(const RunController); return d->delegate; } ContextMenuExtension RunController::contextMenuExtension(Context* ctx, QWidget* parent) { Q_D(RunController); d->launchAsInfo.clear(); d->contextItem = nullptr; ContextMenuExtension ext; if( ctx->type() == Context::ProjectItemContext ) { auto* prjctx = static_cast(ctx); if( prjctx->items().count() == 1 ) { ProjectBaseItem* itm = prjctx->items().at( 0 ); int i = 0; for (ILaunchMode* mode : qAsConst(d->launchModes)) { KActionMenu* menu = new KActionMenu(i18n("%1 As...", mode->name() ), parent); const auto types = launchConfigurationTypes(); for (LaunchConfigurationType* type : types) { bool hasLauncher = false; const auto launchers = type->launchers(); for (ILauncher* launcher : launchers) { if( launcher->supportedModes().contains( mode->id() ) ) { hasLauncher = true; } } if( hasLauncher && type->canLaunch(itm) ) { d->launchAsInfo[i] = qMakePair( type->id(), mode->id() ); auto* act = new QAction(menu); act->setText( type->name() ); qCDebug(SHELL) << "Connect " << i << "for action" << act->text() << "in mode" << mode->name(); connect(act, &QAction::triggered, this, [this, i] () { Q_D(RunController); d->launchAs(i); } ); menu->addAction(act); i++; } } if( menu->menu()->actions().count() > 0 ) { ext.addAction( ContextMenuExtension::RunGroup, menu); } else { delete 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; // 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/kdevplatform/shell/sourceformatterjob.cpp b/kdevplatform/shell/sourceformatterjob.cpp index 17059036c8..724291391c 100644 --- a/kdevplatform/shell/sourceformatterjob.cpp +++ b/kdevplatform/shell/sourceformatterjob.cpp @@ -1,153 +1,156 @@ /* * This file is part of KDevelop * * Copyright (C) 2008 Cédric Pasteur * Copyright 2009 Andreas Pakulat * Copyright 2017 Friedrich W. H. Kossebau * * 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 "sourceformatterjob.h" #include "sourceformattercontroller.h" #include #include #include #include #include -#include #include #include #include #include +#include using namespace KDevelop; SourceFormatterJob::SourceFormatterJob(SourceFormatterController* sourceFormatterController) : KJob(sourceFormatterController) , m_sourceFormatterController(sourceFormatterController) , m_workState(WorkIdle) , m_fileIndex(0) { setCapabilities(Killable); // set name for job listing setObjectName(i18n("Reformatting")); KDevelop::ICore::self()->uiController()->registerStatus(this); connect(this, &SourceFormatterJob::finished, this, [this]() { emit hideProgress(this); }); } QString SourceFormatterJob::statusName() const { return i18n("Reformat Files"); } void SourceFormatterJob::doWork() { // TODO: consider to use ExecuteCompositeJob, with every file a separate subjob switch (m_workState) { case WorkIdle: m_workState = WorkFormat; m_fileIndex = 0; emit showProgress(this, 0, 0, 0); emit showMessage(this, i18np("Reformatting one file", "Reformatting %1 files", m_fileList.length())); QMetaObject::invokeMethod(this, "doWork", Qt::QueuedConnection); break; case WorkFormat: if (m_fileIndex < m_fileList.length()) { emit showProgress(this, 0, m_fileList.length(), m_fileIndex); formatFile(m_fileList[m_fileIndex]); // trigger formatting of next file ++m_fileIndex; QMetaObject::invokeMethod(this, "doWork", Qt::QueuedConnection); } else { m_workState = WorkIdle; emitResult(); } break; case WorkCancelled: break; } } void SourceFormatterJob::start() { if (m_workState != WorkIdle) return; m_workState = WorkIdle; QMetaObject::invokeMethod(this, "doWork", Qt::QueuedConnection); } bool SourceFormatterJob::doKill() { m_workState = WorkCancelled; return true; } void SourceFormatterJob::setFiles(const QList& fileList) { m_fileList = fileList; } void SourceFormatterJob::formatFile(const QUrl& url) { // check mimetype QMimeType mime = QMimeDatabase().mimeTypeForUrl(url); qCDebug(SHELL) << "Checking file " << url << " of mime type " << mime.name(); auto formatter = m_sourceFormatterController->formatterForUrl(url, mime); if (!formatter) // unsupported mime type return; // if the file is opened in the editor, format the text in the editor without saving it auto doc = ICore::self()->documentController()->documentForUrl(url); if (doc) { qCDebug(SHELL) << "Processing file " << url << "opened in editor"; m_sourceFormatterController->formatDocument(doc, formatter, mime); return; } qCDebug(SHELL) << "Processing file " << url; auto getJob = KIO::storedGet(url); // TODO: make also async and use start() and integrate using setError and setErrorString. if (getJob->exec()) { // TODO: really fromLocal8Bit/toLocal8Bit? no encoding detection? added in b8062f736a2bf2eec098af531a7fda6ebcdc7cde QString text = QString::fromLocal8Bit(getJob->data()); text = formatter->formatSource(text, url, mime); text = m_sourceFormatterController->addModelineForCurrentLang(text, url, mime); auto putJob = KIO::storedPut(text.toLocal8Bit(), url, -1, KIO::Overwrite); // see getJob - if (!putJob->exec()) - // TODO: integrate with job error reporting, use showErrorMessage? - KMessageBox::error(nullptr, putJob->errorString()); - } else - KMessageBox::error(nullptr, getJob->errorString()); + if (!putJob->exec()) { + auto* message = new Sublime::Message(putJob->errorString(), Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); + } + } else { + auto* message = new Sublime::Message(getJob->errorString(), Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); + } } diff --git a/kdevplatform/shell/uicontroller.cpp b/kdevplatform/shell/uicontroller.cpp index d7f9aec6dc..d15efe6fcf 100644 --- a/kdevplatform/shell/uicontroller.cpp +++ b/kdevplatform/shell/uicontroller.cpp @@ -1,791 +1,803 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * * * 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 "uicontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include "core.h" #include "configpage.h" #include "configdialog.h" #include "debug.h" #include "editorconfigpage.h" #include "shellextension.h" #include "plugincontroller.h" #include "mainwindow.h" #include "workingsetcontroller.h" #include "workingsets/workingset.h" #include "settings/bgpreferences.h" #include "settings/languagepreferences.h" #include "settings/environmentpreferences.h" #include "settings/pluginpreferences.h" #include "settings/projectpreferences.h" #include "settings/sourceformattersettings.h" #include "settings/uipreferences.h" #include "settings/templateconfig.h" #include "settings/analyzerspreferences.h" #include "settings/documentationpreferences.h" #include "settings/runtimespreferences.h" namespace KDevelop { class UiControllerPrivate { public: UiControllerPrivate(Core* core, UiController* controller) : core(core) , areasRestored(false) , m_controller(controller) { if (Core::self()->workingSetControllerInternal()) Core::self()->workingSetControllerInternal()->initializeController(m_controller); m_controller->connect(m_controller, &Sublime::Controller::mainWindowAdded, m_controller, &UiController::mainWindowAdded); QMap desired; desired[QStringLiteral("org.kdevelop.ClassBrowserView")] = Sublime::Left; desired[QStringLiteral("org.kdevelop.DocumentsView")] = Sublime::Left; desired[QStringLiteral("org.kdevelop.ProjectsView")] = Sublime::Left; desired[QStringLiteral("org.kdevelop.FileManagerView")] = Sublime::Left; desired[QStringLiteral("org.kdevelop.ProblemReporterView")] = Sublime::Bottom; desired[QStringLiteral("org.kdevelop.OutputView")] = Sublime::Bottom; desired[QStringLiteral("org.kdevelop.ContextBrowser")] = Sublime::Bottom; desired[QStringLiteral("org.kdevelop.KonsoleView")] = Sublime::Bottom; desired[QStringLiteral("org.kdevelop.SnippetView")] = Sublime::Right; desired[QStringLiteral("org.kdevelop.ExternalScriptView")] = Sublime::Right; desired[QStringLiteral("org.kdevelop.ScratchpadView")] = Sublime::Left; Sublime::Area* a = new Sublime::Area(m_controller, QStringLiteral("code"), i18n("Code")); a->setDesiredToolViews(desired); a->setIconName(QStringLiteral("document-edit")); m_controller->addDefaultArea(a); desired.clear(); desired[QStringLiteral("org.kdevelop.debugger.VariablesView")] = Sublime::Left; desired[QStringLiteral("org.kdevelop.debugger.BreakpointsView")] = Sublime::Bottom; desired[QStringLiteral("org.kdevelop.debugger.StackView")] = Sublime::Bottom; desired[QStringLiteral("org.kdevelop.debugger.ConsoleView")] = Sublime::Bottom; desired[QStringLiteral("org.kdevelop.KonsoleView")] = Sublime::Bottom; a = new Sublime::Area(m_controller, QStringLiteral("debug"), i18n("Debug")); a->setDesiredToolViews(desired); a->setIconName(QStringLiteral("debug-run")); m_controller->addDefaultArea(a); desired.clear(); desired[QStringLiteral("org.kdevelop.ProjectsView")] = Sublime::Left; desired[QStringLiteral("org.kdevelop.PatchReview")] = Sublime::Bottom; a = new Sublime::Area(m_controller, QStringLiteral("review"), i18n("Review")); a->setDesiredToolViews(desired); a->setIconName(QStringLiteral("text-x-patch")); m_controller->addDefaultArea(a); if(!(Core::self()->setupFlags() & Core::NoUi)) { defaultMainWindow = new MainWindow(m_controller); m_controller->addMainWindow(defaultMainWindow); activeSublimeWindow = defaultMainWindow; } else { activeSublimeWindow = defaultMainWindow = nullptr; } m_assistantTimer.setSingleShot(true); m_assistantTimer.setInterval(100); } void widgetChanged(QWidget*, QWidget* now) { if (now) { auto* win = qobject_cast(now->window()); if( win ) { activeSublimeWindow = win; } } } Core* const core; QPointer defaultMainWindow; QHash factoryDocuments; QPointer activeSublimeWindow; bool areasRestored; /// QWidget implementing IToolViewActionListener interface, or null QPointer activeActionListener; QTimer m_assistantTimer; private: UiController *m_controller; }; class UiToolViewFactory: public Sublime::ToolFactory { public: explicit UiToolViewFactory(IToolViewFactory *factory): m_factory(factory) {} ~UiToolViewFactory() override { delete m_factory; } QWidget* create(Sublime::ToolDocument *doc, QWidget *parent = nullptr) override { Q_UNUSED( doc ); return m_factory->create(parent); } QList< QAction* > contextMenuActions(QWidget* viewWidget) const override { return m_factory->contextMenuActions( viewWidget ); } QList toolBarActions( QWidget* viewWidget ) const override { return m_factory->toolBarActions( viewWidget ); } QString id() const override { return m_factory->id(); } private: IToolViewFactory* const m_factory; }; class ViewSelectorItem: public QListWidgetItem { public: explicit ViewSelectorItem(const QString& text, IToolViewFactory* factory, QListWidget* parent = nullptr, int type = Type) : QListWidgetItem(text, parent, type) , factory(factory) {} IToolViewFactory* const factory; }; class NewToolViewListWidget: public QListWidget { Q_OBJECT public: explicit NewToolViewListWidget(MainWindow *mw, QWidget* parent = nullptr) :QListWidget(parent), m_mw(mw) { connect(this, &NewToolViewListWidget::doubleClicked, this, &NewToolViewListWidget::addNewToolViewByDoubleClick); } Q_SIGNALS: void addNewToolView(MainWindow *mw, QListWidgetItem *item); private Q_SLOTS: void addNewToolViewByDoubleClick(const QModelIndex& index) { QListWidgetItem *item = itemFromIndex(index); // Disable item so that the tool view can not be added again. item->setFlags(item->flags() & ~Qt::ItemIsEnabled); emit addNewToolView(m_mw, item); } private: MainWindow* const m_mw; }; UiController::UiController(Core *core) : Sublime::Controller(nullptr), IUiController() , d_ptr(new UiControllerPrivate(core, this)) { setObjectName(QStringLiteral("UiController")); if (!defaultMainWindow() || (Core::self()->setupFlags() & Core::NoUi)) return; connect(qApp, &QApplication::focusChanged, this, [this] (QWidget* old, QWidget* now) { Q_D(UiController); d->widgetChanged(old, now); } ); setupActions(); } UiController::~UiController() = default; void UiController::setupActions() { } void UiController::mainWindowAdded(Sublime::MainWindow* mainWindow) { connect(mainWindow, &MainWindow::activeToolViewChanged, this, &UiController::slotActiveToolViewChanged); connect(mainWindow, &MainWindow::areaChanged, this, &UiController::slotAreaChanged); // also check after area reconstruction } // FIXME: currently, this always create new window. Probably, // should just rename it. void UiController::switchToArea(const QString &areaName, SwitchMode switchMode) { if (switchMode == ThisWindow) { showArea(areaName, activeSublimeWindow()); return; } auto *main = new MainWindow(this); addMainWindow(main); showArea(areaName, main); main->initialize(); // WTF? First, enabling this code causes crashes since we // try to disconnect some already-deleted action, or something. // Second, this code will disconnection the clients from guiFactory // of the previous main window. Ick! #if 0 //we need to add all existing guiclients to the new mainwindow //@todo adymo: add only ones that belong to the area (when the area code is there) const auto clients = oldMain->guiFactory()->clients(); for (KXMLGUIClient *client : clients) { main->guiFactory()->addClient(client); } #endif main->show(); } QWidget* UiController::findToolView(const QString& name, IToolViewFactory *factory, FindFlags flags) { Q_D(UiController); if(!d->areasRestored || !activeArea()) return nullptr; const QList views = activeArea()->toolViews(); for (Sublime::View* view : views) { auto* doc = qobject_cast(view->document()); if(doc && doc->title() == name && view->widget()) { if(flags & Raise) view->requestRaise(); return view->widget(); } } QWidget* ret = nullptr; if(flags & Create) { Sublime::ToolDocument* doc = d->factoryDocuments.value(factory); if(!doc) { doc = new Sublime::ToolDocument(name, this, new UiToolViewFactory(factory)); d->factoryDocuments.insert(factory, doc); } Sublime::View* view = addToolViewToArea(factory, doc, activeArea()); if(view) ret = view->widget(); if(flags & Raise) findToolView(name, factory, Raise); } return ret; } void UiController::raiseToolView(QWidget* toolViewWidget) { Q_D(UiController); if(!d->areasRestored) return; const QList views = activeArea()->toolViews(); for (Sublime::View* view : views) { if(view->widget() == toolViewWidget) { view->requestRaise(); return; } } } void UiController::addToolView(const QString & name, IToolViewFactory *factory, FindFlags state) { Q_D(UiController); if (!factory) return; qCDebug(SHELL) ; auto *doc = new Sublime::ToolDocument(name, this, new UiToolViewFactory(factory)); d->factoryDocuments[factory] = doc; /* Until areas are restored, we don't know which views should be really added, and which not, so we just record view availability. */ if (d->areasRestored && state != None) { const auto areas = allAreas(); for (Sublime::Area* area : areas) { addToolViewToArea(factory, doc, area); } } } void KDevelop::UiController::raiseToolView(Sublime::View * view) { const auto areas = allAreas(); for (Sublime::Area* area : areas) { if( area->toolViews().contains( view ) ) area->raiseToolView( view ); } slotActiveToolViewChanged(view); } void UiController::slotAreaChanged(Sublime::Area*) { // this slot gets call if an area in *any* MainWindow changed // so let's first get the "active area" const auto area = activeSublimeWindow()->area(); if (area) { // walk through shown tool views and maku sure the const auto shownIds = area->shownToolViews(Sublime::AllPositions); for (Sublime::View* toolView : qAsConst(area->toolViews())) { if (shownIds.contains(toolView->document()->documentSpecifier())) { slotActiveToolViewChanged(toolView); } } } } void UiController::slotActiveToolViewChanged(Sublime::View* view) { Q_D(UiController); if (!view) { return; } // record the last active tool view action listener if (qobject_cast(view->widget())) { d->activeActionListener = view->widget(); } } void KDevelop::UiController::removeToolView(IToolViewFactory *factory) { Q_D(UiController); if (!factory) return; qCDebug(SHELL) ; //delete the tooldocument Sublime::ToolDocument *doc = d->factoryDocuments.value(factory); ///@todo adymo: on document deletion all its views shall be also deleted for (Sublime::View* view : doc->views()) { const auto areas = allAreas(); for (Sublime::Area *area : areas) { if (area->removeToolView(view)) view->deleteLater(); } } d->factoryDocuments.remove(factory); delete doc; } Sublime::Area *UiController::activeArea() { Sublime::MainWindow *m = activeSublimeWindow(); if (m) return activeSublimeWindow()->area(); return nullptr; } Sublime::MainWindow *UiController::activeSublimeWindow() { Q_D(UiController); return d->activeSublimeWindow; } MainWindow *UiController::defaultMainWindow() { Q_D(UiController); return d->defaultMainWindow; } void UiController::initialize() { defaultMainWindow()->initialize(); } void UiController::cleanup() { for (Sublime::MainWindow* w : mainWindows()) { w->saveSettings(); } saveAllAreas(KSharedConfig::openConfig()); } void UiController::selectNewToolViewToAdd(MainWindow *mw) { Q_D(UiController); if (!mw || !mw->area()) return; ScopedDialog dia(mw); dia->setWindowTitle(i18n("Select Tool View to Add")); auto mainLayout = new QVBoxLayout(dia); auto *list = new NewToolViewListWidget(mw, dia); list->setSelectionMode(QAbstractItemView::ExtendedSelection); list->setSortingEnabled(true); for (QHash::const_iterator it = d->factoryDocuments.constBegin(); it != d->factoryDocuments.constEnd(); ++it) { ViewSelectorItem *item = new ViewSelectorItem(it.value()->title(), it.key(), list); if (!item->factory->allowMultiple() && toolViewPresent(it.value(), mw->area())) { // Disable item if the tool view is already present. item->setFlags(item->flags() & ~Qt::ItemIsEnabled); } list->addItem(item); } list->setFocus(); connect(list, &NewToolViewListWidget::addNewToolView, this, &UiController::addNewToolView); mainLayout->addWidget(list); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dia->connect(buttonBox, &QDialogButtonBox::accepted, dia.data(), &QDialog::accept); dia->connect(buttonBox, &QDialogButtonBox::rejected, dia.data(), &QDialog::reject); mainLayout->addWidget(buttonBox); if (dia->exec() == QDialog::Accepted) { const auto items = list->selectedItems(); for (QListWidgetItem* item : items) { addNewToolView(mw, item); } } } void UiController::addNewToolView(MainWindow *mw, QListWidgetItem* item) { Q_D(UiController); auto *current = static_cast(item); Sublime::ToolDocument *doc = d->factoryDocuments[current->factory]; Sublime::View *view = doc->createView(); mw->area()->addToolView(view, Sublime::dockAreaToPosition(current->factory->defaultPosition())); current->factory->viewCreated(view); } void UiController::showSettingsDialog() { ConfigDialog cfgDlg(activeMainWindow()); auto editorConfigPage = new EditorConfigPage(&cfgDlg); auto languageConfigPage = new LanguagePreferences(&cfgDlg); auto analyzersPreferences = new AnalyzersPreferences(&cfgDlg); auto documentationPreferences = new DocumentationPreferences(&cfgDlg); auto runtimesPreferences = new RuntimesPreferences(&cfgDlg); auto templateConfig = new TemplateConfig(&cfgDlg); const auto configPages = QVector { new UiPreferences(&cfgDlg), new PluginPreferences(&cfgDlg), new SourceFormatterSettings(&cfgDlg), new ProjectPreferences(&cfgDlg), new EnvironmentPreferences(QString(), &cfgDlg), templateConfig, documentationPreferences, analyzersPreferences, runtimesPreferences, languageConfigPage, editorConfigPage }; for (auto page : configPages) { cfgDlg.appendConfigPage(page); } cfgDlg.appendSubConfigPage(languageConfigPage, new BGPreferences(&cfgDlg)); auto addPluginPages = [&](IPlugin* plugin) { for (int i = 0, numPages = plugin->configPages(); i < numPages; ++i) { auto page = plugin->configPage(i, &cfgDlg); if (!page) continue; if (page->configPageType() == ConfigPage::LanguageConfigPage) { cfgDlg.appendSubConfigPage(languageConfigPage, page); } else if (page->configPageType() == ConfigPage::AnalyzerConfigPage) { cfgDlg.appendSubConfigPage(analyzersPreferences, page); } else if (page->configPageType() == ConfigPage::RuntimeConfigPage) { cfgDlg.appendSubConfigPage(runtimesPreferences, page); } else if (page->configPageType() == ConfigPage::DocumentationConfigPage) { cfgDlg.appendSubConfigPage(documentationPreferences, page); } else { cfgDlg.insertConfigPage(editorConfigPage, page); } } }; auto plugins = ICore::self()->pluginController()->loadedPlugins(); std::sort(plugins.begin(), plugins.end()); for (IPlugin* plugin : qAsConst(plugins)) { addPluginPages(plugin); } // TODO: only load settings if a UI related page was changed? connect(&cfgDlg, &ConfigDialog::configSaved, activeSublimeWindow(), &Sublime::MainWindow::loadSettings); // make sure that pages get added whenever a new plugin is loaded (probably from the plugin selection dialog) // removal on plugin unload is already handled in ConfigDialog connect(ICore::self()->pluginController(), &IPluginController::pluginLoaded, &cfgDlg, addPluginPages); cfgDlg.exec(); } Sublime::Controller* UiController::controller() { return this; } KParts::MainWindow *UiController::activeMainWindow() { return activeSublimeWindow(); } void UiController::saveArea(Sublime::Area * area, KConfigGroup & group) { area->save(group); if (!area->workingSet().isEmpty()) { WorkingSet* set = Core::self()->workingSetControllerInternal()->workingSet(area->workingSet()); set->saveFromArea(area, area->rootIndex()); } } void UiController::loadArea(Sublime::Area * area, const KConfigGroup & group) { area->load(group); if (!area->workingSet().isEmpty()) { WorkingSet* set = Core::self()->workingSetControllerInternal()->workingSet(area->workingSet()); Q_ASSERT(set->isConnected(area)); Q_UNUSED(set); } } void UiController::saveAllAreas(const KSharedConfigPtr& config) { KConfigGroup uiConfig(config, "User Interface"); int wc = mainWindows().size(); uiConfig.writeEntry("Main Windows Count", wc); for (int w = 0; w < wc; ++w) { KConfigGroup mainWindowConfig(&uiConfig, QStringLiteral("Main Window %1").arg(w)); for (Sublime::Area* defaultArea : defaultAreas()) { // FIXME: using object name seems ugly. QString type = defaultArea->objectName(); Sublime::Area* area = this->area(w, type); KConfigGroup areaConfig(&mainWindowConfig, QLatin1String("Area ") + type); areaConfig.deleteGroup(); areaConfig.writeEntry("id", type); saveArea(area, areaConfig); areaConfig.sync(); } } uiConfig.sync(); } void UiController::loadAllAreas(const KSharedConfigPtr& config) { Q_D(UiController); KConfigGroup uiConfig(config, "User Interface"); int wc = uiConfig.readEntry("Main Windows Count", 1); /* It is expected the main windows are restored before restoring areas. */ if (wc > mainWindows().size()) wc = mainWindows().size(); /* Offer all tool views to the default areas. */ for (Sublime::Area* area : defaultAreas()) { QHash::const_iterator i, e; for (i = d->factoryDocuments.constBegin(), e = d->factoryDocuments.constEnd(); i != e; ++i) { addToolViewIfWanted(i.key(), i.value(), area); } } /* Restore per-windows areas. */ for (int w = 0; w < wc; ++w) { KConfigGroup mainWindowConfig(&uiConfig, QStringLiteral("Main Window %1").arg(w)); Sublime::MainWindow *mw = mainWindows()[w]; /* We loop over default areas. This means that if the config file has an area of some type that is not in default set, we'd just ignore it. I think it's fine -- the model were a given mainwindow can has it's own area types not represented in the default set is way too complex. */ for (Sublime::Area* defaultArea : defaultAreas()) { QString type = defaultArea->objectName(); Sublime::Area* area = this->area(w, type); KConfigGroup areaConfig(&mainWindowConfig, QLatin1String("Area ") + type); qCDebug(SHELL) << "Trying to restore area " << type; /* This is just an easy check that a group exists, to avoid "restoring" area from empty config group, wiping away programmatically installed defaults. */ if (areaConfig.readEntry("id", "") == type) { qCDebug(SHELL) << "Restoring area " << type; loadArea(area, areaConfig); } // At this point we know which tool views the area wants. // Tender all tool views we have. QHash::const_iterator i, e; for (i = d->factoryDocuments.constBegin(), e = d->factoryDocuments.constEnd(); i != e; ++i) { addToolViewIfWanted(i.key(), i.value(), area); } } // Force reload of the changes. showAreaInternal(mw->area(), mw); mw->enableAreaSettingsSave(); } d->areasRestored = true; } void UiController::addToolViewToDockArea(IToolViewFactory* factory, Qt::DockWidgetArea area) { Q_D(UiController); addToolViewToArea(factory, d->factoryDocuments.value(factory), activeArea(), Sublime::dockAreaToPosition(area)); } bool UiController::toolViewPresent(Sublime::ToolDocument* doc, Sublime::Area* area) { for (Sublime::View *view : doc->views()) { if( area->toolViews().contains( view ) ) return true; } return false; } void UiController::addToolViewIfWanted(IToolViewFactory* factory, Sublime::ToolDocument* doc, Sublime::Area* area) { if (area->wantToolView(factory->id())) { addToolViewToArea(factory, doc, area); } } Sublime::View* UiController::addToolViewToArea(IToolViewFactory* factory, Sublime::ToolDocument* doc, Sublime::Area* area, Sublime::Position p) { Sublime::View* view = doc->createView(); area->addToolView( view, p == Sublime::AllPositions ? Sublime::dockAreaToPosition(factory->defaultPosition()) : p); connect(view, &Sublime::View::raise, this, QOverload::of(&UiController::raiseToolView)); factory->viewCreated(view); return view; } void UiController::registerStatus(QObject* status) { Sublime::MainWindow* w = activeSublimeWindow(); if (!w) return; auto* mw = qobject_cast(w); if (!mw) return; mw->registerStatus(status); } void UiController::showErrorMessage(const QString& message, int timeout) { Sublime::MainWindow* w = activeSublimeWindow(); if (!w) return; auto* mw = qobject_cast(w); if (!mw) return; QMetaObject::invokeMethod(mw, "showErrorMessage", Q_ARG(QString, message), Q_ARG(int, timeout)); } +void UiController::postMessage(Sublime::Message* message) +{ + // if Core has flag Core::NoUi there also is no window, so catched as well here + Sublime::MainWindow* window = activeSublimeWindow(); + if (!window) { + delete message; + return; + } + QMetaObject::invokeMethod(window, "postMessage", Q_ARG(Sublime::Message*, message)); +} + const QHash< IToolViewFactory*, Sublime::ToolDocument* >& UiController::factoryDocuments() const { Q_D(const UiController); return d->factoryDocuments; } QWidget* UiController::activeToolViewActionListener() const { Q_D(const UiController); return d->activeActionListener; } QList UiController::allAreas() const { return Sublime::Controller::allAreas(); } } #include "uicontroller.moc" #include "moc_uicontroller.cpp" diff --git a/kdevplatform/shell/uicontroller.h b/kdevplatform/shell/uicontroller.h index 4008a271dc..44a12cb6b5 100644 --- a/kdevplatform/shell/uicontroller.h +++ b/kdevplatform/shell/uicontroller.h @@ -1,131 +1,132 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * * * 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_UICONTROLLER_H #define KDEVPLATFORM_UICONTROLLER_H #include "shellexport.h" #include #include #include #include class QListWidgetItem; namespace Sublime { class ToolDocument; } namespace KDevelop { class Core; class MainWindow; class UiControllerPrivate; class KDEVPLATFORMSHELL_EXPORT UiController: public Sublime::Controller, public IUiController { Q_OBJECT public: explicit UiController(Core *core); ~UiController() override; /** @return area for currently active sublime mainwindow or 0 if no sublime mainwindow is active.*/ Sublime::Area *activeArea() override; /** @return active sublime mainwindow or 0 if no such mainwindow is active.*/ virtual Sublime::MainWindow *activeSublimeWindow(); /** @return active sublime mainwindow or 0 if no such mainwindow is active.*/ KParts::MainWindow *activeMainWindow() override; /** @return default main window - the main window for default area in the shell. No guarantee is given that it always exists so this method may return 0.*/ MainWindow *defaultMainWindow(); void switchToArea(const QString &areaName, SwitchMode switchMode) override; void addToolView(const QString &name, IToolViewFactory *factory, FindFlags state) override; void removeToolView(IToolViewFactory *factory) override; QWidget* findToolView(const QString& name, IToolViewFactory *factory, FindFlags flags) override; void raiseToolView(QWidget* toolViewWidget) override; void selectNewToolViewToAdd(MainWindow *mw); void initialize(); void cleanup(); void showSettingsDialog(); Sublime::Controller* controller() override; void mainWindowAdded(Sublime::MainWindow* mainWindow); void saveAllAreas(const KSharedConfigPtr& config); void saveArea(Sublime::Area* area, KConfigGroup & group); void loadAllAreas(const KSharedConfigPtr& config); void loadArea(Sublime::Area* area, const KConfigGroup & group); /*! @p status must implement KDevelop::IStatus */ void registerStatus(QObject* status) override; void showErrorMessage(const QString& message, int timeout) override; + void postMessage(Sublime::Message* message) override; /// Returns list of available view factories together with their ToolDocuments. /// @see addToolView(), removeToolView(), findToolView() const QHash& factoryDocuments() const; /// Adds a tool view in the active area to the dock area @p area. /// @see activeArea() void addToolViewToDockArea(KDevelop::IToolViewFactory* factory, Qt::DockWidgetArea area); bool toolViewPresent(Sublime::ToolDocument* doc, Sublime::Area* area); QWidget* activeToolViewActionListener() const override; QList allAreas() const override; public Q_SLOTS: void raiseToolView(Sublime::View * view); private Q_SLOTS: void addNewToolView(MainWindow* mw, QListWidgetItem* item); void slotAreaChanged(Sublime::Area* area); void slotActiveToolViewChanged(Sublime::View* view); private: void addToolViewIfWanted(IToolViewFactory* factory, Sublime::ToolDocument* doc, Sublime::Area* area); Sublime::View* addToolViewToArea(IToolViewFactory* factory, Sublime::ToolDocument* doc, Sublime::Area* area, Sublime::Position p=Sublime::AllPositions); void setupActions(); private: const QScopedPointer d_ptr; Q_DECLARE_PRIVATE(UiController) friend class UiControllerPrivate; }; } #endif diff --git a/kdevplatform/sublime/CMakeLists.txt b/kdevplatform/sublime/CMakeLists.txt index 6485dbaadf..4608c08d83 100644 --- a/kdevplatform/sublime/CMakeLists.txt +++ b/kdevplatform/sublime/CMakeLists.txt @@ -1,57 +1,61 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevplatform\") add_subdirectory(examples) if(BUILD_TESTING) add_subdirectory(tests) endif() set(sublime_LIB_SRCS area.cpp areaindex.cpp container.cpp controller.cpp document.cpp mainwindow.cpp mainwindow_p.cpp mainwindowoperator.cpp urldocument.cpp tooldocument.cpp view.cpp viewbarcontainer.cpp sublimedefs.cpp aggregatemodel.cpp holdupdates.cpp idealcontroller.cpp ideallayout.cpp idealtoolbutton.cpp idealdockwidget.cpp idealbuttonbarwidget.cpp + + message.cpp + messagewidget.cpp ) declare_qt_logging_category(sublime_LIB_SRCS TYPE LIBRARY CATEGORY_BASENAME "sublime" ) kdevplatform_add_library(KDevPlatformSublime SOURCES ${sublime_LIB_SRCS}) target_link_libraries(KDevPlatformSublime PUBLIC KF5::Parts PRIVATE KF5::KIOWidgets ) install(FILES area.h areaindex.h areawalkers.h container.h controller.h document.h mainwindow.h mainwindowoperator.h + message.h urldocument.h sublimedefs.h tooldocument.h view.h viewbarcontainer.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/sublime COMPONENT Devel) diff --git a/kdevplatform/sublime/mainwindow.cpp b/kdevplatform/sublime/mainwindow.cpp index 0b36845894..697eaa0d8d 100644 --- a/kdevplatform/sublime/mainwindow.cpp +++ b/kdevplatform/sublime/mainwindow.cpp @@ -1,492 +1,499 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * 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 "mainwindow.h" #include "mainwindow_p.h" #include #include #include #include #include #include #include #include #include #include "area.h" #include "view.h" #include "controller.h" #include "container.h" #include "idealbuttonbarwidget.h" #include "idealcontroller.h" #include "holdupdates.h" #include namespace Sublime { MainWindow::MainWindow(Controller *controller, Qt::WindowFlags flags) : KParts::MainWindow(nullptr, flags) , d_ptr(new MainWindowPrivate(this, controller)) { connect(this, &MainWindow::destroyed, controller, QOverload<>::of(&Controller::areaReleased)); loadGeometry(KSharedConfig::openConfig()->group("Main Window")); // don't allow AllowTabbedDocks - that doesn't make sense for "ideal" UI setDockOptions(QMainWindow::AnimatedDocks); } bool MainWindow::containsView(View* view) const { const auto areas = this->areas(); return std::any_of(areas.begin(), areas.end(), [view](Area* area) { return area->views().contains(view); }); } QList< Area* > MainWindow::areas() const { QList< Area* > areas = controller()->areas(const_cast(this)); if(areas.isEmpty()) areas = controller()->defaultAreas(); return areas; } MainWindow::~MainWindow() { qCDebug(SUBLIME) << "destroying mainwindow"; } void MainWindow::reconstructViews(const QList& topViews) { Q_D(MainWindow); d->reconstructViews(topViews); } QList MainWindow::topViews() const { Q_D(const MainWindow); QList topViews; const auto views = d->area->views(); for (View* view : views) { if(view->hasWidget()) { QWidget* widget = view->widget(); if(widget->parent() && widget->parent()->parent()) { auto* container = qobject_cast(widget->parent()->parent()); if(container->currentWidget() == widget) topViews << view; } } } return topViews; } QList MainWindow::containers() const { Q_D(const MainWindow); return d->viewContainers.values(); } void MainWindow::setArea(Area *area) { Q_D(MainWindow); if (d->area) disconnect(d->area, nullptr, d, nullptr); bool differentArea = (area != d->area); /* All views will be removed from dock area now. However, this does not mean those are removed from area, so prevent slotDockShown from recording those views as no longer shown in the area. */ d->ignoreDockShown = true; if (d->autoAreaSettingsSave && differentArea) saveSettings(); HoldUpdates hu(this); if (d->area) clearArea(); d->area = area; d->reconstruct(); if(d->area->activeView()) activateView(d->area->activeView()); else d->activateFirstVisibleView(); initializeStatusBar(); emit areaChanged(area); d->ignoreDockShown = false; hu.stop(); loadSettings(); connect(area, &Area::viewAdded, d, &MainWindowPrivate::viewAdded); connect(area, &Area::viewRemoved, d, &MainWindowPrivate::viewRemovedInternal); connect(area, &Area::requestToolViewRaise, d, &MainWindowPrivate::raiseToolView); connect(area, &Area::aboutToRemoveView, d, &MainWindowPrivate::aboutToRemoveView); connect(area, &Area::toolViewAdded, d, &MainWindowPrivate::toolViewAdded); connect(area, &Area::aboutToRemoveToolView, d, &MainWindowPrivate::aboutToRemoveToolView); connect(area, &Area::toolViewMoved, d, &MainWindowPrivate::toolViewMoved); } void MainWindow::initializeStatusBar() { //nothing here, reimplement in the subclasses if you want to have status bar //inside the bottom tool view buttons row } void MainWindow::clearArea() { Q_D(MainWindow); emit areaCleared(d->area); d->clearArea(); } QList MainWindow::toolDocks() const { Q_D(const MainWindow); return d->docks; } Area *Sublime::MainWindow::area() const { Q_D(const MainWindow); return d->area; } Controller *MainWindow::controller() const { Q_D(const MainWindow); return d->controller; } View *MainWindow::activeView() const { Q_D(const MainWindow); return d->activeView; } View *MainWindow::activeToolView() const { Q_D(const MainWindow); return d->activeToolView; } void MainWindow::activateView(Sublime::View* view, bool focus) { Q_D(MainWindow); const auto containerIt = d->viewContainers.constFind(view); if (containerIt == d->viewContainers.constEnd()) return; if (d->activeView == view) { if (focus && view && !view->widget()->hasFocus()) view->widget()->setFocus(); return; } (*containerIt)->setCurrentWidget(view->widget()); setActiveView(view, focus); d->area->setActiveView(view); } void MainWindow::setActiveView(View *view, bool focus) { Q_D(MainWindow); View* oldActiveView = d->activeView; d->activeView = view; if (focus && view && !view->widget()->hasFocus()) view->widget()->setFocus(); if(d->activeView != oldActiveView) emit activeViewChanged(view); } void Sublime::MainWindow::setActiveToolView(View *view) { Q_D(MainWindow); d->activeToolView = view; emit activeToolViewChanged(view); } void MainWindow::saveSettings() { Q_D(MainWindow); d->disableConcentrationMode(); QString group = QStringLiteral("MainWindow"); if (area()) group += QLatin1Char('_') + area()->objectName(); KConfigGroup cg = KSharedConfig::openConfig()->group(group); // This will try to save also the window size and the enabled state of the statusbar. // But it's OK, since we won't use this information when loading. saveMainWindowSettings(cg); //debugToolBar visibility is stored separately to allow a area dependent default value const auto toolBars = this->toolBars(); for (KToolBar* toolbar : toolBars) { if (toolbar->objectName() == QLatin1String("debugToolBar")) { cg.writeEntry("debugToolBarVisibility", toolbar->isVisibleTo(this)); } } d->idealController->leftBarWidget->saveOrderSettings(cg); d->idealController->bottomBarWidget->saveOrderSettings(cg); d->idealController->rightBarWidget->saveOrderSettings(cg); cg.sync(); } void MainWindow::loadSettings() { Q_D(MainWindow); HoldUpdates hu(this); qCDebug(SUBLIME) << "loading settings for " << (area() ? area()->objectName() : QString()); QString group = QStringLiteral("MainWindow"); if (area()) group += QLatin1Char('_') + area()->objectName(); KConfigGroup cg = KSharedConfig::openConfig()->group(group); // What follows is copy-paste from applyMainWindowSettings. Unfortunately, // we don't really want that one to try restoring window size, and we also // cannot stop it from doing that in any clean way. // We also do not want that one do it for the enabled state of the statusbar: // KMainWindow scans the widget tree for a QStatusBar-inheriting instance and // set enabled state by the config value stored by the key "StatusBar", // while the QStatusBar subclass used in sublime should always be enabled. auto* mb = findChild(); if (mb) { QString entry = cg.readEntry ("MenuBar", "Enabled"); if ( entry == QLatin1String("Disabled") ) mb->hide(); else mb->show(); } if ( !autoSaveSettings() || cg.name() == autoSaveGroup() ) { QString entry = cg.readEntry ("ToolBarsMovable", "Enabled"); if ( entry == QLatin1String("Disabled") ) KToolBar::setToolBarsLocked(true); else KToolBar::setToolBarsLocked(false); } // Utilise the QMainWindow::restoreState() functionality // Note that we're fixing KMainWindow bug here -- the original // code has this fragment above restoring toolbar properties. // As result, each save/restore would move the toolbar a bit to // the left. if (cg.hasKey("State")) { QByteArray state; state = cg.readEntry("State", state); state = QByteArray::fromBase64(state); // One day will need to load the version number, but for now, assume 0 restoreState(state); } else { // If there's no state we use a default size of 870x650 // Resize only when showing "code" area. If we do that for other areas, // then we'll hit bug https://bugs.kde.org/show_bug.cgi?id=207990 // TODO: adymo: this is more like a hack, we need a proper first-start initialization if (area() && area()->objectName() == QLatin1String("code")) resize(870,650); } int n = 1; // Toolbar counter. toolbars are counted from 1, const auto toolBars = this->toolBars(); for (KToolBar* toolbar : toolBars) { QString group(QStringLiteral("Toolbar")); // Give a number to the toolbar, but prefer a name if there is one, // because there's no real guarantee on the ordering of toolbars group += (toolbar->objectName().isEmpty() ? QString::number(n) : QLatin1Char(' ')+toolbar->objectName()); KConfigGroup toolbarGroup(&cg, group); toolbar->applySettings(toolbarGroup); if (toolbar->objectName() == QLatin1String("debugToolBar")) { //debugToolBar visibility is stored separately to allow a area dependent default value bool visibility = cg.readEntry("debugToolBarVisibility", area()->objectName() == QLatin1String("debug")); toolbar->setVisible(visibility); } n++; } const bool tabBarHidden = !Container::configTabBarVisible(); const bool closeButtonsOnTabs = Container::configCloseButtonsOnTabs(); for (Container *container : qAsConst(d->viewContainers)) { container->setTabBarHidden(tabBarHidden); container->setCloseButtonsOnTabs(closeButtonsOnTabs); } hu.stop(); d->idealController->leftBarWidget->loadOrderSettings(cg); d->idealController->bottomBarWidget->loadOrderSettings(cg); d->idealController->rightBarWidget->loadOrderSettings(cg); emit settingsLoaded(); d->disableConcentrationMode(); } bool MainWindow::queryClose() { // saveSettings(); KConfigGroup config(KSharedConfig::openConfig(), "Main Window"); saveGeometry(config); config.sync(); return KParts::MainWindow::queryClose(); } +void MainWindow::postMessage(Message* message) +{ + Q_D(MainWindow); + + d->postMessage(message); +} + QString MainWindow::screenKey() const { const int scnum = QApplication::desktop()->screenNumber(parentWidget()); QList screens = QApplication::screens(); QRect desk = screens[scnum]->geometry(); // if the desktop is virtual then use virtual screen size if (QGuiApplication::primaryScreen()->virtualSiblings().size() > 1) desk = QGuiApplication::primaryScreen()->virtualGeometry(); return QStringLiteral("Desktop %1 %2") .arg(desk.width()).arg(desk.height()); } void MainWindow::saveGeometry(KConfigGroup &config) const { config.writeEntry(screenKey(), geometry()); } void MainWindow::loadGeometry(const KConfigGroup &config) { // The below code, essentially, is copy-paste from // KMainWindow::restoreWindowSize. Right now, that code is buggy, // as per http://permalink.gmane.org/gmane.comp.kde.devel.core/52423 // so we implement a less theoretically correct, but working, version // below QRect g = config.readEntry(screenKey(), QRect()); if (!g.isEmpty()) setGeometry(g); } void MainWindow::enableAreaSettingsSave() { Q_D(MainWindow); d->autoAreaSettingsSave = true; } QWidget *MainWindow::statusBarLocation() const { Q_D(const MainWindow); return d->idealController->statusBarLocation(); } ViewBarContainer *MainWindow::viewBarContainer() const { Q_D(const MainWindow); return d->viewBarContainer; } void MainWindow::setTabBarLeftCornerWidget(QWidget* widget) { Q_D(MainWindow); d->setTabBarLeftCornerWidget(widget); } void MainWindow::tabDoubleClicked(View* view) { Q_UNUSED(view); Q_D(MainWindow); d->toggleDocksShown(); } void MainWindow::tabContextMenuRequested(View* , QMenu* ) { // do nothing } void MainWindow::tabToolTipRequested(View*, Container*, int) { // do nothing } void MainWindow::newTabRequested() { } void MainWindow::dockBarContextMenuRequested(Qt::DockWidgetArea , const QPoint& ) { // do nothing } View* MainWindow::viewForPosition(const QPoint& globalPos) const { Q_D(const MainWindow); for (Container* container : qAsConst(d->viewContainers)) { QRect globalGeom = QRect(container->mapToGlobal(QPoint(0,0)), container->mapToGlobal(QPoint(container->width(), container->height()))); if(globalGeom.contains(globalPos)) { return d->widgetToView[container->currentWidget()]; } } return nullptr; } void MainWindow::setBackgroundCentralWidget(QWidget* w) { Q_D(MainWindow); d->setBackgroundCentralWidget(w); } } #include "moc_mainwindow.cpp" diff --git a/kdevplatform/sublime/mainwindow.h b/kdevplatform/sublime/mainwindow.h index 81e6ba5b90..d986daf1fc 100644 --- a/kdevplatform/sublime/mainwindow.h +++ b/kdevplatform/sublime/mainwindow.h @@ -1,185 +1,188 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * 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_SUBLIMEMAINWINDOW_H #define KDEVPLATFORM_SUBLIMEMAINWINDOW_H #include #include #include #include "sublimeexport.h" namespace Sublime { class Container; class Area; class View; class Controller; class MainWindowOperator; class ViewBarContainer; +class Message; class MainWindowPrivate; /** @short Sublime Main Window The area-enabled mainwindow to show Sublime views and tool views. To use, a controller and constructed areas are necessary: @code MainWindow w(controller); controller->showArea(area, &w); @endcode */ class KDEVPLATFORMSUBLIME_EXPORT MainWindow: public KParts::MainWindow { Q_OBJECT public: /**Creates a mainwindow and adds it to the controller.*/ explicit MainWindow(Controller *controller, Qt::WindowFlags flags = {}); ~MainWindow() override; /**@return the list of dockwidgets that contain area's tool views.*/ QList toolDocks() const; /**@return area which mainwindow currently shows or 0 if no area has been set.*/ Area *area() const; /**@return controller for this mainwindow.*/ Controller *controller() const; /**@return active view inside this mainwindow.*/ View *activeView() const; /**@return active tool view inside this mainwindow.*/ View *activeToolView() const; /**Enable saving of per-area UI settings (like toolbar properties and position) whenever area is changed. This should be called after all areas are restored, and main window area is set, to prevent saving a half-broken state. */ void enableAreaSettingsSave(); /** Allows setting an additional widget that will be inserted left to the document tab-bar. * The ownership goes to the target. */ void setTabBarLeftCornerWidget(QWidget* widget); /**Sets the area of main window and fills it with views. *The contents is reconstructed, even if the area equals the currently set area. */ void setArea(Area *area); /** * Reconstruct the view structure. This is required after significant untracked changes to the * area-index structure. * Views listed in topViews will be on top of their view stacks. * */ void reconstructViews(const QList& topViews = QList()); /**Returns a list of all views which are on top of their corresponding view stacks*/ QList topViews() const; QList containers() const; /**Returns the view that is closest to the given global position, or zero.*/ View* viewForPosition(const QPoint& globalPos) const; /**Returns true if this main-window contains this view*/ bool containsView(View* view) const; /**Returns all areas that belong to this main-window*/ QList areas() const; /** Sets a @p w widget that will be shown when there are no opened documents. * This method takes the ownership of @p w. */ void setBackgroundCentralWidget(QWidget* w); /**Returns a widget that can hold a centralized view bar*/ ViewBarContainer *viewBarContainer() const; public Q_SLOTS: /**Shows the @p view and makes it active, focusing it by default).*/ void activateView(Sublime::View *view, bool focus = true); + /** Shows the @p message in the message area */ + void postMessage(Sublime::Message* message); /**Loads size/toolbar/menu/statusbar settings to the global configuration file. Reimplement in subclasses to load more and don't forget to call inherited method.*/ virtual void loadSettings(); Q_SIGNALS: /**Emitted before the area is cleared from this mainwindow.*/ void areaCleared(Sublime::Area*); /**Emitted after the new area has been shown in this mainwindow.*/ void areaChanged(Sublime::Area*); /**Emitted when the active view is changed.*/ void activeViewChanged(Sublime::View*); /**Emitted when the active tool view is changed.*/ void activeToolViewChanged(Sublime::View*); /**Emitted when the user interface settings have changed.*/ void settingsLoaded(); /**Emitted when a new view is added to the mainwindow.*/ void viewAdded(Sublime::View*); /**Emitted when a view is going to be removed from the mainwindow.*/ void aboutToRemoveView(Sublime::View*); protected: QWidget *statusBarLocation() const; virtual void initializeStatusBar(); protected Q_SLOTS: virtual void tabDoubleClicked(Sublime::View* view); virtual void tabContextMenuRequested(Sublime::View*, QMenu*); virtual void tabToolTipRequested(Sublime::View* view, Sublime::Container* container, int tab); virtual void newTabRequested(); /**Called whenever the user requests a context menu on a dockwidget bar. You can then e.g. add actions to add dockwidgets. Default implementation does nothing.**/ virtual void dockBarContextMenuRequested(Qt::DockWidgetArea, const QPoint&); public: // FIXME? /**Saves size/toolbar/menu/statusbar settings to the global configuration file. Reimplement in subclasses to save more and don't forget to call inherited method.*/ virtual void saveSettings(); /**Reimplemented to save settings.*/ bool queryClose() override; /** Allow connecting to activateView without the need for a lambda for the default parameter */ void activateViewAndFocus(Sublime::View *view) { activateView(view, true); } private: QString screenKey() const; //Inherit MainWindowOperator to access four methods below /**Unsets the area clearing main window.*/ void clearArea(); /**Sets the active view.*/ void setActiveView(Sublime::View* view, bool focus = true); /**Sets the active tool view and focuses it.*/ void setActiveToolView(View *view); void saveGeometry(KConfigGroup &config) const; void loadGeometry(const KConfigGroup &config); private: const QScopedPointer d_ptr; Q_DECLARE_PRIVATE(MainWindow) friend class MainWindowOperator; friend class MainWindowPrivate; }; } #endif diff --git a/kdevplatform/sublime/mainwindow_p.cpp b/kdevplatform/sublime/mainwindow_p.cpp index 9e831eca12..5496b41737 100644 --- a/kdevplatform/sublime/mainwindow_p.cpp +++ b/kdevplatform/sublime/mainwindow_p.cpp @@ -1,801 +1,851 @@ /*************************************************************************** * Copyright 2006-2009 Alexander Dymo * + * Copyright 2012 Dominik Haumann * + * Copyright 2020 Friedrich W. H. Kossebau * * * * 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 "mainwindow_p.h" #include #include #include #include #include #include #include #include #include #include "area.h" #include "view.h" #include "areaindex.h" #include "document.h" #include "container.h" #include "controller.h" #include "mainwindow.h" #include "viewbarcontainer.h" #include "idealcontroller.h" #include "holdupdates.h" #include "idealbuttonbarwidget.h" +#include "message.h" +#include "messagewidget.h" #include class IdealToolBar : public QToolBar { Q_OBJECT public: explicit IdealToolBar(const QString& title, bool hideWhenEmpty, Sublime::IdealButtonBarWidget* buttons, QMainWindow* parent) : QToolBar(title, parent) , m_buttons(buttons) , m_hideWhenEmpty(hideWhenEmpty) { setMovable(false); setFloatable(false); setObjectName(title); layout()->setMargin(0); addWidget(m_buttons); if (m_hideWhenEmpty) { connect(m_buttons, &Sublime::IdealButtonBarWidget::emptyChanged, this, &IdealToolBar::updateVisibilty); } } private Q_SLOTS: void updateVisibilty() { setVisible(!m_buttons->isEmpty()); } private: Sublime::IdealButtonBarWidget* const m_buttons; const bool m_hideWhenEmpty; }; namespace Sublime { MainWindowPrivate::MainWindowPrivate(MainWindow *w, Controller* controller) :controller(controller), area(nullptr), activeView(nullptr), activeToolView(nullptr), bgCentralWidget(nullptr), ignoreDockShown(false), autoAreaSettingsSave(false), m_mainWindow(w) { KActionCollection *ac = m_mainWindow->actionCollection(); m_concentrationModeAction = new QAction(i18n("Concentration Mode"), this); m_concentrationModeAction->setIcon(QIcon::fromTheme(QStringLiteral("page-zoom"))); m_concentrationModeAction->setToolTip(i18n("Removes most of the controls so you can focus on what matters.")); m_concentrationModeAction->setCheckable(true); m_concentrationModeAction->setChecked(false); ac->setDefaultShortcut(m_concentrationModeAction, Qt::META | Qt::Key_C); connect(m_concentrationModeAction, &QAction::toggled, this, &MainWindowPrivate::restoreConcentrationMode); ac->addAction(QStringLiteral("toggle_concentration_mode"), m_concentrationModeAction); QAction* action = new QAction(i18n("Show Left Dock"), this); action->setCheckable(true); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_Left); connect(action, &QAction::toggled, this, &MainWindowPrivate::showLeftDock); ac->addAction(QStringLiteral("show_left_dock"), action); action = new QAction(i18n("Show Right Dock"), this); action->setCheckable(true); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_Right); connect(action, &QAction::toggled, this, &MainWindowPrivate::showRightDock); ac->addAction(QStringLiteral("show_right_dock"), action); action = new QAction(i18n("Show Bottom Dock"), this); action->setCheckable(true); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_Down); connect(action, &QAction::toggled, this, &MainWindowPrivate::showBottomDock); ac->addAction(QStringLiteral("show_bottom_dock"), action); action = new QAction(i18nc("@action", "Focus Editor"), this); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_E); connect(action, &QAction::triggered, this, &MainWindowPrivate::focusEditor); ac->addAction(QStringLiteral("focus_editor"), action); action = new QAction(i18n("Hide/Restore Docks"), this); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_Up); connect(action, &QAction::triggered, this, &MainWindowPrivate::toggleDocksShown); ac->addAction(QStringLiteral("hide_all_docks"), action); action = new QAction(i18n("Next Tool View"), this); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_N); action->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); connect(action, &QAction::triggered, this, &MainWindowPrivate::selectNextDock); ac->addAction(QStringLiteral("select_next_dock"), action); action = new QAction(i18n("Previous Tool View"), this); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_P); action->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); connect(action, &QAction::triggered, this, &MainWindowPrivate::selectPreviousDock); ac->addAction(QStringLiteral("select_previous_dock"), action); action = new KActionMenu(i18n("Tool Views"), this); ac->addAction(QStringLiteral("docks_submenu"), action); idealController = new IdealController(m_mainWindow); m_leftToolBar = new IdealToolBar(i18n("Left Button Bar"), true, idealController->leftBarWidget, m_mainWindow); m_mainWindow->addToolBar(Qt::LeftToolBarArea, m_leftToolBar); m_rightToolBar = new IdealToolBar(i18n("Right Button Bar"), true, idealController->rightBarWidget, m_mainWindow); m_mainWindow->addToolBar(Qt::RightToolBarArea, m_rightToolBar); m_bottomToolBar = new IdealToolBar(i18n("Bottom Button Bar"), false, idealController->bottomBarWidget, m_mainWindow); m_mainWindow->addToolBar(Qt::BottomToolBarArea, m_bottomToolBar); // adymo: intentionally do not add a toolbar for top buttonbar // this doesn't work well with toolbars added via xmlgui centralWidget = new QWidget; centralWidget->setObjectName(QStringLiteral("centralWidget")); auto* layout = new QVBoxLayout(centralWidget); layout->setMargin(0); centralWidget->setLayout(layout); + messageWidget = new MessageWidget(); + layout->addWidget(messageWidget); + splitterCentralWidget = new QSplitter(centralWidget); // take as much space as possible splitterCentralWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layout->addWidget(splitterCentralWidget, 2); // this view bar container is used for the ktexteditor integration to show // all view bars at a central place, esp. for split view configurations viewBarContainer = new ViewBarContainer; viewBarContainer->setObjectName(QStringLiteral("viewBarContainer")); // hide by default viewBarContainer->setVisible(false); // only take as much as needed viewBarContainer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); layout->addWidget(viewBarContainer); m_mainWindow->setCentralWidget(centralWidget); connect(idealController, &IdealController::dockShown, this, &MainWindowPrivate::slotDockShown); connect(idealController, &IdealController::dockBarContextMenuRequested, m_mainWindow, &MainWindow::dockBarContextMenuRequested); } MainWindowPrivate::~MainWindowPrivate() { + // create working copy as messages are auto-removing themselves from the hash on destruction + const auto messages = m_messageHash.keys(); + qDeleteAll(messages); + delete m_leftTabbarCornerWidget.data(); m_leftTabbarCornerWidget.clear(); } void MainWindowPrivate::disableConcentrationMode() { m_concentrationModeAction->setChecked(false); restoreConcentrationMode(); } void MainWindowPrivate::restoreConcentrationMode() { const bool concentrationModeOn = m_concentrationModeAction->isChecked(); QWidget* cornerWidget = nullptr; if (m_concentrateToolBar) { QLayout* l = m_concentrateToolBar->layout(); QLayoutItem* li = l->takeAt(1); //ensure the cornerWidget isn't destroyed with the toolbar if (li) { cornerWidget = li->widget(); delete li; } m_concentrateToolBar->deleteLater(); } m_mainWindow->menuBar()->setVisible(!concentrationModeOn); m_bottomToolBar->setVisible(!concentrationModeOn); m_leftToolBar->setVisible(!concentrationModeOn); m_rightToolBar->setVisible(!concentrationModeOn); if (concentrationModeOn) { m_concentrateToolBar = new QToolBar(m_mainWindow); m_concentrateToolBar->setObjectName(QStringLiteral("concentrateToolBar")); m_concentrateToolBar->addAction(m_concentrationModeAction); m_concentrateToolBar->toggleViewAction()->setVisible(false); auto *action = new QWidgetAction(this); action->setDefaultWidget(m_mainWindow->menuBar()->cornerWidget(Qt::TopRightCorner)); m_concentrateToolBar->addAction(action); m_concentrateToolBar->setMovable(false); m_mainWindow->addToolBar(Qt::TopToolBarArea, m_concentrateToolBar); m_mainWindow->menuBar()->setCornerWidget(nullptr, Qt::TopRightCorner); } else if (cornerWidget) { m_mainWindow->menuBar()->setCornerWidget(cornerWidget, Qt::TopRightCorner); cornerWidget->show(); } if (concentrationModeOn) { m_mainWindow->installEventFilter(this); } else { m_mainWindow->removeEventFilter(this); } } bool MainWindowPrivate::eventFilter(QObject* obj, QEvent* event) { Q_ASSERT(m_mainWindow == obj); if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) { const auto ev = static_cast(event); Qt::KeyboardModifiers modifiers = ev->modifiers(); //QLineEdit banned mostly so that alt navigation can be used from QuickOpen const bool visible = modifiers == Qt::AltModifier && ev->type() == QEvent::KeyPress && !qApp->focusWidget()->inherits("QLineEdit"); m_mainWindow->menuBar()->setVisible(visible); } return false; } void MainWindowPrivate::showLeftDock(bool b) { idealController->showLeftDock(b); } void MainWindowPrivate::showBottomDock(bool b) { idealController->showBottomDock(b); } void MainWindowPrivate::showRightDock(bool b) { idealController->showRightDock(b); } void MainWindowPrivate::setBackgroundCentralWidget(QWidget* w) { delete bgCentralWidget; QLayout* l=m_mainWindow->centralWidget()->layout(); l->addWidget(w); bgCentralWidget=w; setBackgroundVisible(area->views().isEmpty()); } void MainWindowPrivate::setBackgroundVisible(bool v) { if(!bgCentralWidget) return; bgCentralWidget->setVisible(v); splitterCentralWidget->setVisible(!v); } void MainWindowPrivate::focusEditor() { if (View* view = m_mainWindow->activeView()) if (view->hasWidget()) view->widget()->setFocus(Qt::ShortcutFocusReason); } void MainWindowPrivate::toggleDocksShown() { idealController->toggleDocksShown(); } void MainWindowPrivate::selectNextDock() { idealController->goPrevNextDock(IdealController::NextDock); } void MainWindowPrivate::selectPreviousDock() { idealController->goPrevNextDock(IdealController::PrevDock); } Area::WalkerMode MainWindowPrivate::IdealToolViewCreator::operator() (View *view, Sublime::Position position) { if (!d->docks.contains(view)) { d->docks << view; //add view d->idealController->addView(d->positionToDockArea(position), view); } return Area::ContinueWalker; } Area::WalkerMode MainWindowPrivate::ViewCreator::operator() (AreaIndex *index) { QSplitter *splitter = d->m_indexSplitters.value(index); if (!splitter) { //no splitter - we shall create it and populate with views if (!index->parent()) { qCDebug(SUBLIME) << "reconstructing root area"; //this is root area splitter = d->splitterCentralWidget; d->m_indexSplitters[index] = splitter; } else { if (!d->m_indexSplitters.value(index->parent())) { // can happen in working set code, as that adds a view to a child index first // hence, recursively reconstruct the parent indices first operator()(index->parent()); } QSplitter *parent = d->m_indexSplitters.value(index->parent()); splitter = new QSplitter(parent); d->m_indexSplitters[index] = splitter; if(index == index->parent()->first()) parent->insertWidget(0, splitter); else parent->addWidget(splitter); } Q_ASSERT(splitter); } if (index->isSplit()) //this is a visible splitter splitter->setOrientation(index->orientation()); else { Container *container = nullptr; while(splitter->count() && qobject_cast(splitter->widget(0))) { // After unsplitting, we might have to remove old splitters QWidget* widget = splitter->widget(0); qCDebug(SUBLIME) << "deleting" << widget; widget->setParent(nullptr); delete widget; } if (!splitter->widget(0)) { //we need to create view container container = new Container(splitter); connect(container, &Container::activateView, d->m_mainWindow, &MainWindow::activateViewAndFocus); connect(container, &Container::tabDoubleClicked, d->m_mainWindow, &MainWindow::tabDoubleClicked); connect(container, &Container::tabContextMenuRequested, d->m_mainWindow, &MainWindow::tabContextMenuRequested); connect(container, &Container::tabToolTipRequested, d->m_mainWindow, &MainWindow::tabToolTipRequested); connect(container, QOverload::of(&Container::requestClose), d, &MainWindowPrivate::widgetCloseRequest, Qt::QueuedConnection); connect(container, &Container::newTabRequested, d->m_mainWindow, &MainWindow::newTabRequested); splitter->addWidget(container); } else container = qobject_cast(splitter->widget(0)); container->show(); int position = 0; bool hadActiveView = false; Sublime::View* activeView = d->activeView; for (View* view : qAsConst(index->views())) { QWidget *widget = view->widget(container); if (widget) { if(!container->hasWidget(widget)) { container->addWidget(view, position); d->viewContainers[view] = container; d->widgetToView[widget] = view; } if(activeView == view) { hadActiveView = true; container->setCurrentWidget(widget); }else if(topViews.contains(view) && !hadActiveView) container->setCurrentWidget(widget); } position++; } } return Area::ContinueWalker; } void MainWindowPrivate::reconstructViews(const QList& topViews) { ViewCreator viewCreator(this, topViews); area->walkViews(viewCreator, area->rootIndex()); setBackgroundVisible(area->views().isEmpty()); } void MainWindowPrivate::reconstruct() { if(m_leftTabbarCornerWidget) { m_leftTabbarCornerWidget->hide(); m_leftTabbarCornerWidget->setParent(nullptr); } IdealToolViewCreator toolViewCreator(this); area->walkToolViews(toolViewCreator, Sublime::AllPositions); reconstructViews(); { QSignalBlocker blocker(m_mainWindow); qCDebug(SUBLIME) << "RECONSTRUCT" << area << area->shownToolViews(Sublime::Left); for (View* view : qAsConst(area->toolViews())) { QString id = view->document()->documentSpecifier(); if (!id.isEmpty()) { Sublime::Position pos = area->toolViewPosition(view); if (area->shownToolViews(pos).contains(id)) idealController->raiseView(view, IdealController::GroupWithOtherViews); } } } setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); } void MainWindowPrivate::clearArea() { if(m_leftTabbarCornerWidget) m_leftTabbarCornerWidget->setParent(nullptr); //reparent tool view widgets to nullptr to prevent their deletion together with dockwidgets for (View* view : qAsConst(area->toolViews())) { // FIXME should we really delete here?? bool nonDestructive = true; idealController->removeView(view, nonDestructive); if (view->hasWidget()) view->widget()->setParent(nullptr); } docks.clear(); //reparent all view widgets to 0 to prevent their deletion together with central //widget. this reparenting is necessary when switching areas inside the same mainwindow const auto views = area->views(); for (View* view : views) { if (view->hasWidget()) view->widget()->setParent(nullptr); } cleanCentralWidget(); m_mainWindow->setActiveView(nullptr); m_indexSplitters.clear(); area = nullptr; viewContainers.clear(); setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); } void MainWindowPrivate::cleanCentralWidget() { while(splitterCentralWidget->count()) delete splitterCentralWidget->widget(0); setBackgroundVisible(true); } struct ShownToolViewFinder { ShownToolViewFinder() {} Area::WalkerMode operator()(View *v, Sublime::Position /*position*/) { if (v->hasWidget() && v->widget()->isVisible()) views << v; return Area::ContinueWalker; } QList views; }; void MainWindowPrivate::slotDockShown(Sublime::View* /*view*/, Sublime::Position pos, bool /*shown*/) { if (ignoreDockShown) return; ShownToolViewFinder finder; m_mainWindow->area()->walkToolViews(finder, pos); QStringList ids; ids.reserve(finder.views.size()); for (View* v : qAsConst(finder.views)) { ids << v->document()->documentSpecifier(); } area->setShownToolViews(pos, ids); } void MainWindowPrivate::viewRemovedInternal(AreaIndex* index, View* view) { Q_UNUSED(index); Q_UNUSED(view); setBackgroundVisible(area->views().isEmpty()); } void MainWindowPrivate::viewAdded(Sublime::AreaIndex *index, Sublime::View *view) { if(m_leftTabbarCornerWidget) { m_leftTabbarCornerWidget->hide(); m_leftTabbarCornerWidget->setParent(nullptr); } // Remove container objects in the hierarchy from the parents, // because they are not needed anymore, and might lead to broken splitter hierarchy and crashes. for(Sublime::AreaIndex* current = index; current; current = current->parent()) { QSplitter *splitter = m_indexSplitters[current]; if (current->isSplit() && splitter) { // Also update the orientation splitter->setOrientation(current->orientation()); for(int w = 0; w < splitter->count(); ++w) { auto *container = qobject_cast(splitter->widget(w)); //we need to remove extra container before reconstruction //first reparent widgets in container so that they are not deleted if(container) { while (container->count()) { container->widget(0)->setParent(nullptr); } //and then delete the container delete container; } } } } ViewCreator viewCreator(this); area->walkViews(viewCreator, index); emit m_mainWindow->viewAdded( view ); setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); setBackgroundVisible(false); } void Sublime::MainWindowPrivate::raiseToolView(Sublime::View * view) { idealController->raiseView(view); } void MainWindowPrivate::aboutToRemoveView(Sublime::AreaIndex *index, Sublime::View *view) { QSplitter *splitter = m_indexSplitters[index]; if (!splitter) return; qCDebug(SUBLIME) << "index " << index << " root " << area->rootIndex(); qCDebug(SUBLIME) << "splitter " << splitter << " container " << splitter->widget(0); qCDebug(SUBLIME) << "structure: " << index->print() << " whole structure: " << area->rootIndex()->print(); //find the container for the view and remove the widget auto *container = qobject_cast(splitter->widget(0)); if (!container) { qCWarning(SUBLIME) << "Splitter does not have a left widget!"; return; } emit m_mainWindow->aboutToRemoveView( view ); if (view->widget()) widgetToView.remove(view->widget()); viewContainers.remove(view); const bool wasActive = m_mainWindow->activeView() == view; if (container->count() > 1) { //container is not empty or this is a root index //just remove a widget if( view->widget() ) { container->removeWidget(view->widget()); view->widget()->setParent(nullptr); //activate what is visible currently in the container if the removed view was active if (wasActive) { m_mainWindow->setActiveView(container->viewForWidget(container->currentWidget())); return; } } } else { if(m_leftTabbarCornerWidget) { m_leftTabbarCornerWidget->hide(); m_leftTabbarCornerWidget->setParent(nullptr); } // We've about to remove the last view of this container. It will // be empty, so have to delete it, as well. // If we have a container, then it should be the only child of // the splitter. Q_ASSERT(splitter->count() == 1); container->removeWidget(view->widget()); if (view->widget()) view->widget()->setParent(nullptr); else qCWarning(SUBLIME) << "View does not have a widget!"; Q_ASSERT(container->count() == 0); // We can be called from signal handler of container // (which is tab widget), so defer deleting it. container->deleteLater(); container->setParent(nullptr); /* If we're not at the top level, we get to collapse split views. */ if (index->parent()) { /* The splitter used to have container as the only child, now it's time to get rid of it. Make sure deleting splitter does not delete container -- per above comment, we'll delete it later. */ container->setParent(nullptr); m_indexSplitters.remove(index); delete splitter; AreaIndex *parent = index->parent(); QSplitter *parentSplitter = m_indexSplitters[parent]; AreaIndex *sibling = parent->first() == index ? parent->second() : parent->first(); QSplitter *siblingSplitter = m_indexSplitters[sibling]; if(siblingSplitter) { HoldUpdates du(parentSplitter); //save sizes and orientation of the sibling splitter parentSplitter->setOrientation(siblingSplitter->orientation()); QList sizes = siblingSplitter->sizes(); /* Parent has two children -- 'index' that we've deleted and 'sibling'. We move all children of 'sibling' into parent, and delete 'sibling'. sibling either contains a single Container instance, or a bunch of further QSplitters. */ while (siblingSplitter->count() > 0) { //reparent contents into parent splitter QWidget *siblingWidget = siblingSplitter->widget(0); siblingWidget->setParent(parentSplitter); parentSplitter->addWidget(siblingWidget); } m_indexSplitters.remove(sibling); delete siblingSplitter; parentSplitter->setSizes(sizes); } qCDebug(SUBLIME) << "after deletion " << parent << " has " << parentSplitter->count() << " elements"; //find the container somewhere to activate auto *containerToActivate = parentSplitter->findChild(); //activate the current view there if (containerToActivate) { m_mainWindow->setActiveView(containerToActivate->viewForWidget(containerToActivate->currentWidget())); setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); return; } } } setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); if ( wasActive ) { m_mainWindow->setActiveView(nullptr); } } void MainWindowPrivate::toolViewAdded(Sublime::View* /*toolView*/, Sublime::Position position) { IdealToolViewCreator toolViewCreator(this); area->walkToolViews(toolViewCreator, position); } void MainWindowPrivate::aboutToRemoveToolView(Sublime::View *toolView, Sublime::Position /*position*/) { if (!docks.contains(toolView)) return; idealController->removeView(toolView); // TODO are Views unique? docks.removeAll(toolView); } void MainWindowPrivate::toolViewMoved( Sublime::View *toolView, Sublime::Position position) { if (!docks.contains(toolView)) return; idealController->moveView(toolView, positionToDockArea(position)); } Qt::DockWidgetArea MainWindowPrivate::positionToDockArea(Position position) { switch (position) { case Sublime::Left: return Qt::LeftDockWidgetArea; case Sublime::Right: return Qt::RightDockWidgetArea; case Sublime::Bottom: return Qt::BottomDockWidgetArea; case Sublime::Top: return Qt::TopDockWidgetArea; default: return Qt::LeftDockWidgetArea; } } void MainWindowPrivate::updateAreaSwitcher(Sublime::Area *area) { QAction* action = m_areaActions.value(area); if (action) action->setChecked(true); } void MainWindowPrivate::activateFirstVisibleView() { QList views = area->views(); if (views.count() > 0) m_mainWindow->activateView(views.first()); } void MainWindowPrivate::widgetCloseRequest(QWidget* widget) { if (View *view = widgetToView.value(widget)) { area->closeView(view); } } void MainWindowPrivate::setTabBarLeftCornerWidget(QWidget* widget) { if(widget != m_leftTabbarCornerWidget.data()) { delete m_leftTabbarCornerWidget.data(); m_leftTabbarCornerWidget.clear(); } m_leftTabbarCornerWidget = widget; if(!widget || !area || viewContainers.isEmpty()) return; AreaIndex* putToIndex = area->rootIndex(); QSplitter* splitter = m_indexSplitters[putToIndex]; while(putToIndex->isSplit()) { putToIndex = putToIndex->first(); splitter = m_indexSplitters[putToIndex]; } // Q_ASSERT(splitter || putToIndex == area->rootIndex()); Container* c = nullptr; if(splitter) { c = qobject_cast(splitter->widget(0)); }else{ c = *viewContainers.constBegin(); } Q_ASSERT(c); c->setLeftCornerWidget(widget); } +void MainWindowPrivate::postMessage(Message* message) +{ + if (!message) { + return; + } + + message->setParent(this); + + // if there are no actions, add a close action by default if widget does not auto-hide + if (message->actions().isEmpty() && message->autoHide() < 0) { + auto* closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), + i18nc("@action", "Close")); + closeAction->setToolTip(i18nc("@info:tooltip", "Close message")); + message->addAction(closeAction); + } + + // reparent actions, as we want full control over when they are deleted + QVector> managedMessageActions; + const auto messageActions = message->actions(); + managedMessageActions.reserve(messageActions.size()); + for (QAction* action : messageActions) { + action->setParent(nullptr); + managedMessageActions.append(QSharedPointer(action)); + } + m_messageHash.insert(message, managedMessageActions); + + // also catch if the user manually calls delete message + connect(message, &Message::closed, this, &MainWindowPrivate::messageDestroyed); + + messageWidget->postMessage(message, managedMessageActions); +} + +void MainWindowPrivate::messageDestroyed(Message* message) +{ + // Message is already in destructor + Q_ASSERT(m_messageHash.contains(message)); + m_messageHash.remove(message); +} + } #include "mainwindow_p.moc" #include "moc_mainwindow_p.cpp" diff --git a/kdevplatform/sublime/mainwindow_p.h b/kdevplatform/sublime/mainwindow_p.h index df63d30875..4dd18d66da 100644 --- a/kdevplatform/sublime/mainwindow_p.h +++ b/kdevplatform/sublime/mainwindow_p.h @@ -1,153 +1,162 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * 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_SUBLIMEMAINWINDOW_P_H #define KDEVPLATFORM_SUBLIMEMAINWINDOW_P_H #include #include #include #include #include "area.h" #include "sublimedefs.h" #include "mainwindow.h" #include class QAction; class QSplitter; class IdealToolBar; namespace Sublime { class View; class Container; class Controller; class AreaIndex; class IdealMainWidget; class IdealController; +class MessageWidget; +class Message; class MainWindowPrivate: public QObject { Q_OBJECT public: MainWindowPrivate(MainWindow *w, Controller* controller); ~MainWindowPrivate() override; /**Use this to create tool views for an area.*/ class IdealToolViewCreator { public: explicit IdealToolViewCreator(MainWindowPrivate *_d): d(_d) {} Area::WalkerMode operator() (View *view, Sublime::Position position); private: MainWindowPrivate* const d; }; /**Use this to create views for an area.*/ class ViewCreator { public: explicit ViewCreator(MainWindowPrivate *_d, const QList& _topViews = QList()): d(_d), topViews(_topViews.toSet()) {} Area::WalkerMode operator() (AreaIndex *index); private: MainWindowPrivate* const d; const QSet topViews; }; /**Reconstructs the mainwindow according to the current area.*/ void reconstruct(); /**Reconstructs the views according to the current area index.*/ void reconstructViews(const QList& topViews = QList()); /**Clears the area leaving mainwindow empty.*/ void clearArea(); /** Sets a @p w widget that will be shown when there are no documents on the area */ void setBackgroundCentralWidget(QWidget* w); void activateFirstVisibleView(); Controller* const controller; Area *area; QList docks; QMap viewContainers; QMap widgetToView; View *activeView; View *activeToolView; QWidget *centralWidget; QWidget* bgCentralWidget; + MessageWidget* messageWidget; ViewBarContainer* viewBarContainer; QSplitter* splitterCentralWidget; IdealController *idealController; int ignoreDockShown; bool autoAreaSettingsSave; bool eventFilter(QObject* obj, QEvent* event) override; void disableConcentrationMode(); + void postMessage(Message* message); + public Q_SLOTS: void toggleDocksShown(); void viewAdded(Sublime::AreaIndex *index, Sublime::View *view); void viewRemovedInternal(Sublime::AreaIndex *index, Sublime::View *view); void raiseToolView(Sublime::View* view); void aboutToRemoveView(Sublime::AreaIndex *index, Sublime::View *view); void toolViewAdded(Sublime::View *toolView, Sublime::Position position); void aboutToRemoveToolView(Sublime::View *toolView, Sublime::Position position); void toolViewMoved(Sublime::View *toolView, Sublime::Position position); void setTabBarLeftCornerWidget(QWidget* widget); private Q_SLOTS: void updateAreaSwitcher(Sublime::Area *area); void slotDockShown(Sublime::View*, Sublime::Position, bool); void widgetCloseRequest(QWidget* widget); void showLeftDock(bool b); void showRightDock(bool b); void showBottomDock(bool b); void focusEditor(); void selectNextDock(); void selectPreviousDock(); + void messageDestroyed(Message* message); + private: void restoreConcentrationMode(); void setBackgroundVisible(bool v); Qt::DockWidgetArea positionToDockArea(Position position); void cleanCentralWidget(); MainWindow* const m_mainWindow; // uses QPointer to make already-deleted splitters detectable QMap > m_indexSplitters; QMap m_areaActions; QPointer m_leftTabbarCornerWidget; QPointer m_concentrateToolBar; IdealToolBar* m_bottomToolBar; IdealToolBar* m_rightToolBar; IdealToolBar* m_leftToolBar; QAction* m_concentrationModeAction; + + QHash>> m_messageHash; }; } #endif diff --git a/kdevplatform/sublime/message.cpp b/kdevplatform/sublime/message.cpp new file mode 100644 index 0000000000..d18b75a496 --- /dev/null +++ b/kdevplatform/sublime/message.cpp @@ -0,0 +1,141 @@ +/* SPDX-License-Identifier: LGPL-2.0-or-later + + Copyright (C) 2012-2013 Dominik Haumann + Copyright (C) 2020 Friedrich W. H. Kossebau + + 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 "message.h" + +namespace Sublime { + +class MessagePrivate +{ +public: + QVector actions; + QString text; + QIcon icon; + int autoHideDelay = -1; + int priority = 0; + Message::MessageType messageType; + Message::AutoHideMode autoHideMode = Message::AfterUserInteraction; + bool wordWrap = true; +}; + + +Message::Message(const QString& richtext, MessageType type) + : d(new MessagePrivate()) +{ + d->messageType = type; + d->text = richtext; +} + +Message::~Message() +{ + emit closed(this); +} + +QString Message::text() const +{ + return d->text; +} + +void Message::setText(const QString& text) +{ + if (d->text == text) { + return; + } + + d->text = text; + emit textChanged(text); +} + +void Message::setIcon(const QIcon& icon) +{ + d->icon = icon; + emit iconChanged(d->icon); +} + +QIcon Message::icon() const +{ + return d->icon; +} + +Message::MessageType Message::messageType() const +{ + return d->messageType; +} + +void Message::addAction(QAction* action, bool closeOnTrigger) +{ + // make sure this is the parent, so all actions are deleted in the destructor + action->setParent(this); + d->actions.append(action); + + // call close if wanted + if (closeOnTrigger) { + connect(action, &QAction::triggered, + this, &QObject::deleteLater); + } +} + +QVector Message::actions() const +{ + return d->actions; +} + +void Message::setAutoHide(int delay) +{ + d->autoHideDelay = delay; +} + +int Message::autoHide() const +{ + return d->autoHideDelay; +} + +void Message::setAutoHideMode(Message::AutoHideMode mode) +{ + d->autoHideMode = mode; +} + +Message::AutoHideMode Message::autoHideMode() const +{ + return d->autoHideMode; +} + +void Message::setWordWrap(bool wordWrap) +{ + d->wordWrap = wordWrap; +} + +bool Message::wordWrap() const +{ + return d->wordWrap; +} + +void Message::setPriority(int priority) +{ + d->priority = priority; +} + +int Message::priority() const +{ + return d->priority; +} + +} diff --git a/kdevplatform/sublime/message.h b/kdevplatform/sublime/message.h new file mode 100644 index 0000000000..b96d2dd037 --- /dev/null +++ b/kdevplatform/sublime/message.h @@ -0,0 +1,308 @@ +/* SPDX-License-Identifier: LGPL-2.0-or-later + + Copyright (C) 2012-2013 Dominik Haumann + Copyright (C) 2020 Friedrich W. H. Kossebau + + 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. +*/ + +// Forked from KTextEditor::Message at v5.66.0 +// Dropped Document/View properties, made wordWrap true by default, dropped position +// Should be investigated later to turn this and the messagewidget class +// into some reusable generic in-shell-message code, e.g. as KF module + +#ifndef KDEVPLATFORM_SUBLIME_MESSAGE_H +#define KDEVPLATFORM_SUBLIME_MESSAGE_H + +#include "sublimeexport.h" +// Qt +#include +#include +#include +#include + +namespace Sublime +{ + +/** + * @brief This class holds a Message to display in a message area. + * + * @section message_intro Introduction + * + * The Message class holds the data used to display interactive message widgets + * in the shell. Use the MainWindow::postMessage() to post a message as follows: + * + * @code + * // if you keep a pointer after calling postMessage(), + * // always use a QPointer go guard your Message, + * QPointer message = + * new Sublime::Message("text", Sublime::Message::Information); + * message->addAction(...); // add your actions, if any... + * window->postMessage(message); + * @endcode + * + * A Message is deleted automatically if the Message gets closed, meaning that + * you usually can forget the pointer. If you really need to delete a message + * before the user processed it, always guard it with a QPointer! + * + * @section message_creation Message Creation and Deletion + * + * To create a new Message, use code like this: + * @code + * QPointer message = + * new Sublime::Message("My information text", Message::Information); + * // ... + * @endcode + * + * Although discouraged in general, the text of the Message can be changed + * on the fly when it is already visible with setText(). + * + * Once you posted the Message through MainWindow::postMessage(), the + * lifetime depends on the user interaction. The Message gets automatically + * deleted if the user clicks a closing action in the message, or the set + * timeout is reached. + * + * If you posted a message but want to remove it yourself again, just delete + * the message. But beware of the following warning! + * + * @warning Always use QPointer\ to guard the message pointer from + * getting invalid, if you need to access the Message after you posted + * it. + * + * @section message_hiding Autohiding Messages + * + * Message%s can be shown for only a short amount of time by using the autohide + * feature. With setAutoHide() a timeout in milliseconds can be set after which + * the Message is automatically hidden. Further, use setAutoHideMode() to either + * trigger the autohide timer as soon as the widget is shown (AutoHideMode::Immediate), + * or only after user interaction with the view (AutoHideMode::AfterUserInteraction). + * + * The default autohide mode is set to AutoHideMode::AfterUserInteraction. + * This way, it is unlikely the user misses a notification. + */ +class KDEVPLATFORMSUBLIME_EXPORT Message : public QObject +{ + Q_OBJECT + + // + // public data types + // +public: + /** + * Message types used as visual indicator. + * The message types match exactly the behavior of KMessageWidget::MessageType. + * For simple notifications either use Positive or Information. + */ + enum MessageType { + Positive = 0, ///< positive information message + Information, ///< information message type + Warning, ///< warning message type + Error ///< error message type + }; + + /** + * The AutoHideMode determines when to trigger the autoHide timer. + * @see setAutoHide(), autoHide() + */ + enum AutoHideMode { + Immediate = 0, ///< auto-hide is triggered as soon as the message is shown + AfterUserInteraction ///< auto-hide is triggered only after the user interacted with the view + }; + +public: + /** + * Constructor for new messages. + * @param type the message type, e.g. MessageType::Information + * @param richtext text to be displayed + */ + Message(const QString &richtext, MessageType type = Message::Information); + + /** + * Destructor. + */ + ~Message() override; + +public: + /** + * Returns the text set in the constructor. + */ + QString text() const; + + /** + * Returns the icon of this message. + * If the message has no icon set, a null icon is returned. + * @see setIcon() + */ + QIcon icon() const; + + /** + * Returns the message type set in the constructor. + */ + MessageType messageType() const; + + /** + * Adds an action to the message. + * + * By default (@p closeOnTrigger = true), the action closes the message + * displayed. If @p closeOnTrigger is @e false, the message is stays open. + * + * The actions will be displayed in the order you added the actions. + * + * To connect to an action, use the following code: + * @code + * connect(action, &QAction::triggered, receiver, &ReceiverType::slotActionTriggered); + * @endcode + * + * @param action action to be added + * @param closeOnTrigger when triggered, the message widget is closed + * + * @warning The added actions are deleted automatically. + * So do @em not delete the added actions yourself. + */ + void addAction(QAction* action, bool closeOnTrigger = true); + + /** + * Accessor to all actions, mainly used in the internal implementation + * to add the actions into the gui. + * @see addAction() + */ + QVector actions() const; + + /** + * Set the auto hide time to @p delay milliseconds. + * If @p delay < 0, auto hide is disabled. + * If @p delay = 0, auto hide is enabled and set to a sane default + * value of several seconds. + * + * By default, auto hide is disabled. + * + * @see autoHide(), setAutoHideMode() + */ + void setAutoHide(int delay = 0); + + /** + * Returns the auto hide time in milliseconds. + * Please refer to setAutoHide() for an explanation of the return value. + * + * @see setAutoHide(), autoHideMode() + */ + int autoHide() const; + + /** + * Sets the auto hide mode to @p mode. + * The default mode is set to AutoHideMode::AfterUserInteraction. + * @param mode auto hide mode + * @see autoHideMode(), setAutoHide() + */ + void setAutoHideMode(Message::AutoHideMode mode); + + /** + * Get the Message's auto hide mode. + * The default mode is set to AutoHideMode::AfterUserInteraction. + * @see setAutoHideMode(), autoHide() + */ + Message::AutoHideMode autoHideMode() const; + + /** + * Enabled word wrap according to @p wordWrap. + * By default, auto wrap is enabled. + * + * Word wrap is enabled automatically, if the Message's width is larger than + * the parent widget's width to avoid breaking the gui layout. + * + * @see wordWrap() + */ + void setWordWrap(bool wordWrap); + + /** + * Check, whether word wrap is enabled or not. + * + * @see setWordWrap() + */ + bool wordWrap() const; + + /** + * Set the priority of this message to @p priority. + * Messages with higher priority are shown first. + * The default priority is 0. + * + * @see priority() + */ + void setPriority(int priority); + + /** + * Returns the priority of the message. + * + * @see setPriority() + */ + int priority() const; + +public Q_SLOTS: + /** + * Sets the notification contents to @p richtext. + * If the message was already sent through MainWindow::postMessage(), + * the displayed text changes on the fly. + * @note Change text on the fly with care, since changing the text may + * resize the notification widget, which may result in a distracting + * user experience. + * @param richtext new notification text (rich text supported) + * @see textChanged() + */ + void setText(const QString& richtext); + + /** + * Add an optional @p icon for this notification which will be shown next to + * the message text. If the message was already sent through MainWindow::postMessage(), + * the displayed icon changes on the fly. + * @note Change the icon on the fly with care, since changing the text may + * resize the notification widget, which may result in a distracting + * user experience. + * @param icon the icon to be displayed + * @see iconChanged() + */ + void setIcon(const QIcon& icon); + +Q_SIGNALS: + /** + * This signal is emitted before the @p message is deleted. Afterwards, this + * pointer is invalid. + * + * @param message the closed/processed message + */ + void closed(Message* message); + + /** + * This signal is emitted whenever setText() was called. + * + * @param text the new notification text (rich text supported) + * @see setText() + */ + void textChanged(const QString& text); + + /** + * This signal is emitted whenever setIcon() was called. + * @param icon the new notification icon + * @see setIcon() + */ + void iconChanged(const QIcon& icon); + +private: + const QScopedPointer d; +}; + +} + +#endif diff --git a/kdevplatform/sublime/messagewidget.cpp b/kdevplatform/sublime/messagewidget.cpp new file mode 100644 index 0000000000..1a76cdcb3f --- /dev/null +++ b/kdevplatform/sublime/messagewidget.cpp @@ -0,0 +1,284 @@ +/* SPDX-License-Identifier: LGPL-2.0-or-later + + Copyright (C) 2012 Dominik Haumann + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "messagewidget.h" + +// lib +#include "message.h" +// KF +#include +// Qt +#include +#include +#include + +namespace Sublime { + +constexpr int s_defaultAutoHideTime = 6 * 1000; + +// the enums values do not necessarily match, hence translate case-by-case +KMessageWidget::MessageType kMessageWidgetMessageType(Message::MessageType messageType) +{ + return + messageType == Message::Positive ? KMessageWidget::Positive : + messageType == Message::Information ? KMessageWidget::Information : + messageType == Message::Warning ? KMessageWidget::Warning : + messageType == Message::Error ? KMessageWidget::Error : + /* else */ KMessageWidget::Information; +} + + +MessageWidget::MessageWidget(QWidget* parent) + : QWidget(parent) + , m_autoHideTimer(new QTimer(this)) +{ + auto* l = new QVBoxLayout(); + l->setContentsMargins(0, 0, 0, 0); + + m_messageWidget = new KMessageWidget(this); + m_messageWidget->setCloseButtonVisible(false); + + l->addWidget(m_messageWidget); + setLayout(l); + + // tell the widget to always use the minimum size. + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + + // by default, hide widgets + m_messageWidget->hide(); + hide(); + + // setup autoHide timer details + m_autoHideTimer->setSingleShot(true); + + connect(m_messageWidget, &KMessageWidget::linkHovered, + this, &MessageWidget::linkHovered); +} + +void MessageWidget::showNextMessage() +{ + // at this point, we should not have a currently shown message + Q_ASSERT(!m_currentMessage); + + // if not message to show, just stop + if (m_messageQueue.isEmpty()) { + hide(); + return; + } + + // track current message + m_currentMessage = m_messageQueue[0]; + + // set text etc. + m_messageWidget->setText(m_currentMessage->text()); + m_messageWidget->setIcon(m_currentMessage->icon()); + + // connect textChanged() and iconChanged(), so it's possible to change this on the fly + connect(m_currentMessage, &Message::textChanged, + m_messageWidget, &KMessageWidget::setText, Qt::UniqueConnection); + connect(m_currentMessage, &Message::iconChanged, + m_messageWidget, &KMessageWidget::setIcon, Qt::UniqueConnection); + + const KMessageWidget::MessageType widgetMessageType = + kMessageWidgetMessageType(m_currentMessage->messageType()); + m_messageWidget->setMessageType(widgetMessageType); + + // remove all actions from the message widget + const auto messageWidgetActions = m_messageWidget->actions(); + for (QAction* action : messageWidgetActions) { + m_messageWidget->removeAction(action); + } + + // add new actions to the message widget + const auto m_currentMessageActions = m_currentMessage->actions(); + for (QAction* action : m_currentMessageActions) { + m_messageWidget->addAction(action); + } + + // set word wrap of the message + setWordWrap(m_currentMessage); + + // setup auto-hide timer, and start if requested + m_autoHideTime = m_currentMessage->autoHide(); + m_autoHideTimer->stop(); + if (m_autoHideTime >= 0) { + connect(m_autoHideTimer, &QTimer::timeout, + m_currentMessage, &Message::deleteLater, Qt::UniqueConnection); + if (m_currentMessage->autoHideMode() == Message::Immediate) { + m_autoHideTimer->start(m_autoHideTime == 0 ? s_defaultAutoHideTime : m_autoHideTime); + } + } + + // finally show + show(); + // NOTE: use a singleShot timer to avoid resizing issues when showing the message widget the first time (bug #316666) + QTimer::singleShot(0, m_messageWidget, &KMessageWidget::animatedShow); +} + +void MessageWidget::setWordWrap(Message* message) +{ + // want word wrap anyway? -> ok + if (message->wordWrap()) { + m_messageWidget->setWordWrap(message->wordWrap()); + return; + } + + // word wrap not wanted, that's ok if a parent widget does not exist + if (!parentWidget()) { + m_messageWidget->setWordWrap(false); + return; + } + + // word wrap not wanted -> enable word wrap if it breaks the layout otherwise + int margin = 0; + if (parentWidget()->layout()) { + // get left/right margin of the layout, since we need to subtract these + int leftMargin = 0, rightMargin = 0; + parentWidget()->layout()->getContentsMargins(&leftMargin, nullptr, &rightMargin, nullptr); + margin = leftMargin + rightMargin; + } + + // if word wrap enabled, first disable it + if (m_messageWidget->wordWrap()) { + m_messageWidget->setWordWrap(false); + } + + // make sure the widget's size is up-to-date in its hidden state + m_messageWidget->ensurePolished(); + m_messageWidget->adjustSize(); + + // finally enable word wrap, if there is not enough free horizontal space + const int freeSpace = (parentWidget()->width() - margin) - m_messageWidget->width(); + if (freeSpace < 0) { + // qCDebug(LOG_KTE) << "force word wrap to avoid breaking the layout" << freeSpace; + m_messageWidget->setWordWrap(true); + } +} + +void MessageWidget::postMessage(Message* message, const QVector>& actions) +{ + Q_ASSERT(!m_messageHash.contains(message)); + m_messageHash.insert(message, actions); + + // insert message sorted after priority + int i = 0; + for (; i < m_messageQueue.count(); ++i) { + if (message->priority() > m_messageQueue[i]->priority()) { + break; + } + } + + // queue message + m_messageQueue.insert(i, message); + + // catch if the message gets deleted + connect(message, &Message::closed, + this, &MessageWidget::messageDestroyed); + + if (i == 0 && !m_messageWidget->isHideAnimationRunning()) { + // if message has higher priority than the one currently shown, + // then hide the current one and then show the new one. + if (m_currentMessage) { + // autoHide timer may be running for currently shown message, therefore + // simply disconnect autoHide timer to all timeout() receivers + disconnect(m_autoHideTimer, SIGNAL(timeout()), nullptr, nullptr); + m_autoHideTimer->stop(); + + // if there is a current message, the message queue must contain 2 messages + Q_ASSERT(m_messageQueue.size() > 1); + Q_ASSERT(m_currentMessage == m_messageQueue[1]); + + // a bit unnice: disconnect textChanged() and iconChanged() signals of previously visible message + disconnect(m_currentMessage, &Message::textChanged, + m_messageWidget, &KMessageWidget::setText); + disconnect(m_currentMessage, &Message::iconChanged, + m_messageWidget, &KMessageWidget::setIcon); + + m_currentMessage = nullptr; + m_messageWidget->animatedHide(); + } else { + showNextMessage(); + } + } +} + +void MessageWidget::messageDestroyed(Message* message) +{ + // last moment when message is valid, since KTE::Message is already in + // destructor we have to do the following: + // 1. remove message from m_messageQueue, so we don't care about it anymore + // 2. activate hide animation or show a new message() + + // remove widget from m_messageQueue + int i = 0; + for (; i < m_messageQueue.count(); ++i) { + if (m_messageQueue[i] == message) { + break; + } + } + + // the message must be in the list + Q_ASSERT(i < m_messageQueue.count()); + + // remove message + m_messageQueue.removeAt(i); + + // remove message from hash -> release QActions + Q_ASSERT(m_messageHash.contains(message)); + m_messageHash.remove(message); + + // if deleted message is the current message, launch hide animation + if (message == m_currentMessage) { + m_currentMessage = nullptr; + m_messageWidget->animatedHide(); + } +} + +void MessageWidget::startAutoHideTimer() +{ + // message does not want autohide, or timer already running + if (!m_currentMessage // no message, nothing to do + || m_autoHideTime < 0 // message does not want auto-hide + || m_autoHideTimer->isActive() // auto-hide timer is already active + || m_messageWidget->isHideAnimationRunning() // widget is in hide animation phase + || m_messageWidget->isShowAnimationRunning() // widget is in show animation phase + ) { + return; + } + + // safety checks: the message must still be valid + Q_ASSERT(m_messageQueue.size()); + Q_ASSERT(m_currentMessage->autoHide() == m_autoHideTime); + + // start autoHide timer as requested + m_autoHideTimer->start(m_autoHideTime == 0 ? s_defaultAutoHideTime : m_autoHideTime); +} + +void MessageWidget::linkHovered(const QString &link) +{ + QToolTip::showText(QCursor::pos(), link, m_messageWidget); +} + +QString MessageWidget::text() const +{ + return m_messageWidget->text(); +} + +} diff --git a/kdevplatform/sublime/messagewidget.h b/kdevplatform/sublime/messagewidget.h new file mode 100644 index 0000000000..ffccd47259 --- /dev/null +++ b/kdevplatform/sublime/messagewidget.h @@ -0,0 +1,106 @@ +/* SPDX-License-Identifier: LGPL-2.0-or-later + + Copyright (C) 2012 Dominik Haumann + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +// Forked from KTextEditor's KateMessageWidget at v5.66.0 +// Renamed class, dropped fading-enabling KateAnimation proxy + +#ifndef KDEVPLATFORM_SUBLIME_MESSAGEWIDGET_H +#define KDEVPLATFORM_SUBLIME_MESSAGEWIDGET_H + +// Qt +#include +#include +#include +#include +#include + +class KMessageWidget; + +namespace Sublime { + +class Message; + +/** + * This class implements a message widget based on KMessageWidget. + * It is used to show messages through the KTextEditior::MessageInterface. + */ +class MessageWidget : public QWidget +{ + Q_OBJECT + +public: + /** + * Constructor. By default, the widget is hidden. + */ + explicit MessageWidget(QWidget* parent = nullptr); + +public: + /** + * Post a new incoming message. Show either directly, or queue + */ + void postMessage(Message* message, const QVector>& actions); + + // for unit test + QString text() const; + +protected Q_SLOTS: + /** + * Show the next message in the queue. + */ + void showNextMessage(); + + /** + * Helper that enables word wrap to avoid breaking the layout + */ + void setWordWrap(Message* message); + + /** + * catch when a message is deleted, then show next one, if applicable. + */ + void messageDestroyed(Message* message); + /** + * Start autoHide timer if requested + */ + void startAutoHideTimer(); + /** + * User hovers on a link in the message widget. + */ + void linkHovered(const QString& link); + +private: + // sorted list of pending messages + QList m_messageQueue; + // pointer to current Message + QPointer m_currentMessage; + // shared pointers to QActions as guard + QHash>> m_messageHash; + // the message widget, showing the actual contents + KMessageWidget* m_messageWidget; + +private: // some state variables + // autoHide only once user interaction took place + QTimer* m_autoHideTimer; + // flag: save message's autohide time + int m_autoHideTime = -1; +}; + +} + +#endif diff --git a/plugins/appwizard/CMakeLists.txt b/plugins/appwizard/CMakeLists.txt index adb85929a7..3f4d4c5e30 100644 --- a/plugins/appwizard/CMakeLists.txt +++ b/plugins/appwizard/CMakeLists.txt @@ -1,41 +1,46 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevappwizard\") ########### next target ############### set(kdevappwizard_PART_SRCS appwizardplugin.cpp appwizarddialog.cpp appwizardpagewidget.cpp projectselectionpage.cpp projecttemplatesmodel.cpp projectvcspage.cpp ) set(kdevappwizard_PART_UI projectselectionpage.ui projectvcspage.ui ) declare_qt_logging_category(kdevappwizard_PART_SRCS TYPE PLUGIN IDENTIFIER PLUGIN_APPWIZARD CATEGORY_BASENAME "appwizard" ) ki18n_wrap_ui(kdevappwizard_PART_SRCS ${kdevappwizard_PART_UI}) if(NOT KF5_VERSION VERSION_LESS "5.57.0") install(FILES kdevappwizard.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) else() qt5_add_resources(kdevappwizard_PART_SRCS kdevappwizardknsrc.qrc) endif() qt5_add_resources(kdevappwizard_PART_SRCS kdevappwizard.qrc) kdevplatform_add_plugin(kdevappwizard JSON kdevappwizard.json SOURCES ${kdevappwizard_PART_SRCS}) target_link_libraries(kdevappwizard + KDev::Interfaces + KDev::Vcs + KDev::Language + KDev::Sublime + KDev::Util KF5::KIOWidgets KF5::NewStuff KF5::Archive - KDev::Interfaces KDev::Vcs KDev::Language KDev::Util) +) diff --git a/plugins/appwizard/appwizardplugin.cpp b/plugins/appwizard/appwizardplugin.cpp index 3aef0cd389..c1b90c9dd9 100644 --- a/plugins/appwizard/appwizardplugin.cpp +++ b/plugins/appwizard/appwizardplugin.cpp @@ -1,567 +1,570 @@ /*************************************************************************** * Copyright 2001 Bernd Gehrmann * * Copyright 2004-2005 Sascha Cunz * * Copyright 2005 Ian Reinhart Geiser * * Copyright 2007 Alexander Dymo * * Copyright 2008 Evgeniy Ivanov * * * * 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. * * * ***************************************************************************/ #include "appwizardplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include "appwizarddialog.h" #include "projectselectionpage.h" #include "projectvcspage.h" #include "projecttemplatesmodel.h" #include "debug.h" using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(AppWizardFactory, "kdevappwizard.json", registerPlugin();) AppWizardPlugin::AppWizardPlugin(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevappwizard"), parent) , m_templatesModel(nullptr) { setXMLFile(QStringLiteral("kdevappwizard.rc")); m_newFromTemplate = actionCollection()->addAction(QStringLiteral("project_new")); m_newFromTemplate->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template"))); m_newFromTemplate->setText(i18n("New From Template...")); connect(m_newFromTemplate, &QAction::triggered, this, &AppWizardPlugin::slotNewProject); m_newFromTemplate->setToolTip( i18n("Generate a new project from a template") ); m_newFromTemplate->setWhatsThis( i18n("This starts KDevelop's application wizard. " "It helps you to generate a skeleton for your " "application from a set of templates.") ); } AppWizardPlugin::~AppWizardPlugin() { } void AppWizardPlugin::slotNewProject() { model()->refresh(); ScopedDialog dlg(core()->pluginController(), m_templatesModel); if (dlg->exec() == QDialog::Accepted) { QString project = createProject( dlg->appInfo() ); if (!project.isEmpty()) { core()->projectController()->openProject(QUrl::fromLocalFile(project)); KConfig templateConfig(dlg->appInfo().appTemplate); KConfigGroup general(&templateConfig, "General"); const QStringList fileArgs = general.readEntry("ShowFilesAfterGeneration").split(QLatin1Char(','), QString::SkipEmptyParts); for (const auto& fileArg : fileArgs) { QString file = KMacroExpander::expandMacros(fileArg.trimmed(), m_variables); if (QDir::isRelativePath(file)) { file = m_variables[QStringLiteral("PROJECTDIR")] + QLatin1Char('/') + file; } core()->documentController()->openDocument(QUrl::fromUserInput(file)); } } else { - KMessageBox::error( ICore::self()->uiController()->activeMainWindow(), i18n("Could not create project from template\n"), i18n("Failed to create project") ); - } + const QString messageText = i18n("Could not create project from template."); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); + } } } namespace { IDistributedVersionControl* toDVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } ICentralizedVersionControl* toCVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } /*! Trouble while initializing version control. Show failure message to user. */ void vcsError(const QString &errorMsg, QTemporaryDir &tmpdir, const QUrl &dest, const QString &details = QString()) { QString displayDetails = details; if (displayDetails.isEmpty()) { displayDetails = i18n("Please see the Version Control tool view."); } KMessageBox::detailedError(nullptr, errorMsg, displayDetails, i18n("Version Control System Error")); KIO::del(dest, KIO::HideProgressInfo)->exec(); tmpdir.remove(); } /*! Setup distributed version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeDVCS(IDistributedVersionControl* dvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(dvcs); qCDebug(PLUGIN_APPWIZARD) << "DVCS system is used, just initializing DVCS"; const QUrl& dest = info.location; //TODO: check if we want to handle KDevelop project files (like now) or only SRC dir VcsJob* job = dvcs->init(dest); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not initialize DVCS repository"), scratchArea, dest); return false; } qCDebug(PLUGIN_APPWIZARD) << "Initializing DVCS repository:" << dest; job = dvcs->add({dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not add files to the DVCS repository"), scratchArea, dest); return false; } job = dvcs->commit(info.importCommitMessage, {dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not import project into %1.", dvcs->name()), scratchArea, dest, job ? job->errorString() : QString()); return false; } return true; // We're good } /*! Setup version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeCVCS(ICentralizedVersionControl* cvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(cvcs); qCDebug(PLUGIN_APPWIZARD) << "Importing" << info.sourceLocation << "to" << info.repository.repositoryServer(); VcsJob* job = cvcs->import( info.importCommitMessage, QUrl::fromLocalFile(scratchArea.path()), info.repository); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not import project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } qCDebug(PLUGIN_APPWIZARD) << "Checking out"; job = cvcs->createWorkingCopy( info.repository, info.location, IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not checkout imported project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } return true; // initialization phase complete } QString generateIdentifier( const QString& appname ) { QString tmp = appname; QRegExp re(QStringLiteral("[^a-zA-Z0-9_]")); return tmp.replace(re, QStringLiteral("_")); } } // end anonymous namespace QString AppWizardPlugin::createProject(const ApplicationInfo& info) { QFileInfo templateInfo(info.appTemplate); if (!templateInfo.exists()) { qCWarning(PLUGIN_APPWIZARD) << "Project app template does not exist:" << info.appTemplate; return QString(); } QString templateName = templateInfo.baseName(); qCDebug(PLUGIN_APPWIZARD) << "Searching archive for template name:" << templateName; QString templateArchive; const QStringList filters = {templateName + QStringLiteral(".*")}; const QStringList matchesPaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdevappwizard/templates/"), QStandardPaths::LocateDirectory); for (const QString& matchesPath : matchesPaths) { const QStringList files = QDir(matchesPath).entryList(filters); if(!files.isEmpty()) { templateArchive = matchesPath + files.first(); } } if(templateArchive.isEmpty()) { qCWarning(PLUGIN_APPWIZARD) << "Template name does not exist in the template list"; return QString(); } qCDebug(PLUGIN_APPWIZARD) << "Using template archive:" << templateArchive; QUrl dest = info.location; //prepare variable substitution hash m_variables.clear(); m_variables[QStringLiteral("APPNAME")] = info.name; m_variables[QStringLiteral("APPNAMEUC")] = info.name.toUpper(); m_variables[QStringLiteral("APPNAMELC")] = info.name.toLower(); m_variables[QStringLiteral("APPNAMEID")] = generateIdentifier(info.name); m_variables[QStringLiteral("PROJECTDIR")] = dest.toLocalFile(); // backwards compatibility m_variables[QStringLiteral("dest")] = m_variables[QStringLiteral("PROJECTDIR")]; m_variables[QStringLiteral("PROJECTDIRNAME")] = dest.fileName(); m_variables[QStringLiteral("VERSIONCONTROLPLUGIN")] = info.vcsPluginName; KArchive* arch = nullptr; if( templateArchive.endsWith(QLatin1String(".zip")) ) { arch = new KZip(templateArchive); } else { arch = new KTar(templateArchive, QStringLiteral("application/x-bzip")); } if (arch->open(QIODevice::ReadOnly)) { QTemporaryDir tmpdir; QString unpackDir = tmpdir.path(); //the default value for all Centralized VCS IPlugin* plugin = core()->pluginController()->loadPlugin( info.vcsPluginName ); if( info.vcsPluginName.isEmpty() || ( plugin && plugin->extension() ) ) { if( !QFileInfo::exists( dest.toLocalFile() ) ) { QDir::root().mkpath( dest.toLocalFile() ); } unpackDir = dest.toLocalFile(); //in DVCS we unpack template directly to the project's directory } else { QUrl url = KIO::upUrl(dest); if(!QFileInfo::exists(url.toLocalFile())) { QDir::root().mkpath(url.toLocalFile()); } } // estimate metadata files which should not be copied QStringList metaDataFileNames; // try by same name const KArchiveEntry *templateEntry = arch->directory()->entry(templateName + QLatin1String(".kdevtemplate")); // but could be different name, if e.g. downloaded, so make a guess if (!templateEntry || !templateEntry->isFile()) { const auto& entries = arch->directory()->entries(); for (const auto& entryName : entries) { if (entryName.endsWith(QLatin1String(".kdevtemplate"))) { templateEntry = arch->directory()->entry(entryName); break; } } } if (templateEntry && templateEntry->isFile()) { metaDataFileNames << templateEntry->name(); // check if a preview file is to be ignored const auto *templateFile = static_cast(templateEntry); QTemporaryDir temporaryDir; templateFile->copyTo(temporaryDir.path()); KConfig config(temporaryDir.path() + QLatin1Char('/') + templateEntry->name()); KConfigGroup group(&config, "General"); if (group.hasKey("Icon")) { const KArchiveEntry* iconEntry = arch->directory()->entry(group.readEntry("Icon")); if (iconEntry && iconEntry->isFile()) { metaDataFileNames << iconEntry->name(); } } } if (!unpackArchive(arch->directory(), unpackDir, metaDataFileNames)) { QString errorMsg = i18n("Could not create new project"); vcsError(errorMsg, tmpdir, QUrl::fromLocalFile(unpackDir)); return QString(); } if( !info.vcsPluginName.isEmpty() ) { if (!plugin) { // Red Alert, serious program corruption. // This should never happen, the vcs dialog presented a list of vcs // systems and now the chosen system doesn't exist anymore?? tmpdir.remove(); return QString(); } IDistributedVersionControl* dvcs = toDVCS(plugin); ICentralizedVersionControl* cvcs = toCVCS(plugin); bool success = false; if (dvcs) { success = initializeDVCS(dvcs, info, tmpdir); } else if (cvcs) { success = initializeCVCS(cvcs, info, tmpdir); } else { if (KMessageBox::Continue == KMessageBox::warningContinueCancel(nullptr, QStringLiteral("Failed to initialize version control system, " "plugin is neither VCS nor DVCS."))) success = true; } if (!success) return QString(); } tmpdir.remove(); }else { qCDebug(PLUGIN_APPWIZARD) << "failed to open template archive"; return QString(); } QString projectFileName = QDir::cleanPath(dest.toLocalFile() + QLatin1Char('/') + info.name + QLatin1String(".kdev4")); // Loop through the new project directory and try to detect the first .kdev4 file. // If one is found this file will be used. So .kdev4 file can be stored in any subdirectory and the // project templates can be more complex. QDirIterator it(QDir::cleanPath( dest.toLocalFile()), QStringList() << QStringLiteral("*.kdev4"), QDir::NoFilter, QDirIterator::Subdirectories); if(it.hasNext() == true) { projectFileName = it.next(); } qCDebug(PLUGIN_APPWIZARD) << "Returning" << projectFileName << QFileInfo::exists( projectFileName ) ; const QFileInfo projectFileInfo(projectFileName); if (!projectFileInfo.exists()) { qCDebug(PLUGIN_APPWIZARD) << "creating .kdev4 file"; KSharedConfigPtr cfg = KSharedConfig::openConfig( projectFileName, KConfig::SimpleConfig ); KConfigGroup project = cfg->group( "Project" ); project.writeEntry( "Name", info.name ); QString manager = QStringLiteral("KDevGenericManager"); QDir d( dest.toLocalFile() ); const auto data = ICore::self()->pluginController()->queryExtensionPlugins(QStringLiteral("org.kdevelop.IProjectFileManager")); for (const KPluginMetaData& info : data) { QStringList filter = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-ProjectFilesFilter")); if (!filter.isEmpty()) { if (!d.entryList(filter).isEmpty()) { manager = info.pluginId(); break; } } } project.writeEntry( "Manager", manager ); project.sync(); cfg->sync(); KConfigGroup project2 = cfg->group( "Project" ); qCDebug(PLUGIN_APPWIZARD) << "kdev4 file contents:" << project2.readEntry("Name", "") << project2.readEntry("Manager", "" ); } // create developer .kde4 file const QString developerProjectFileName = projectFileInfo.canonicalPath() + QLatin1String("/.kdev4/") + projectFileInfo.fileName(); qCDebug(PLUGIN_APPWIZARD) << "creating developer .kdev4 file:" << developerProjectFileName; KSharedConfigPtr developerCfg = KSharedConfig::openConfig(developerProjectFileName, KConfig::SimpleConfig); KConfigGroup developerProjectGroup = developerCfg->group("Project"); developerProjectGroup.writeEntry("VersionControlSupport", info.vcsPluginName); developerProjectGroup.sync(); developerCfg->sync(); return projectFileName; } bool AppWizardPlugin::unpackArchive(const KArchiveDirectory* dir, const QString& dest, const QStringList& skipList) { qCDebug(PLUGIN_APPWIZARD) << "unpacking dir:" << dir->name() << "to" << dest; const QStringList entries = dir->entries(); qCDebug(PLUGIN_APPWIZARD) << "entries:" << entries.join(QLatin1Char(',')); //This extra tempdir is needed just for the files that have special names, //which may contain macros also files contain content with macros. So the //easiest way to extract the files from the archive and then rename them //and replace the macros is to use a tempdir and copy the file (and //replacing while copying). This also allows one to easily remove all files, //by just unlinking the tempdir QTemporaryDir tdir; bool ret = true; for (const auto& entryName : entries) { if (skipList.contains(entryName)) { continue; } const auto entry = dir->entry(entryName); if (entry->isDirectory()) { const auto* subdir = static_cast(entry); const QString newdest = dest + QLatin1Char('/') + KMacroExpander::expandMacros(subdir->name(), m_variables); if( !QFileInfo::exists( newdest ) ) { QDir::root().mkdir( newdest ); } ret |= unpackArchive(subdir, newdest); } else if (entry->isFile()) { const auto* file = static_cast(entry); file->copyTo(tdir.path()); const QString destName = dest + QLatin1Char('/') + file->name(); if (!copyFileAndExpandMacros(QDir::cleanPath(tdir.path() + QLatin1Char('/') + file->name()), KMacroExpander::expandMacros(destName, m_variables))) { KMessageBox::sorry(nullptr, i18n("The file %1 cannot be created.", dest)); return false; } } } tdir.remove(); return ret; } bool AppWizardPlugin::copyFileAndExpandMacros(const QString &source, const QString &dest) { qCDebug(PLUGIN_APPWIZARD) << "copy:" << source << "to" << dest; QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(source); if( !mime.inherits(QStringLiteral("text/plain")) ) { KIO::CopyJob* job = KIO::copy( QUrl::fromUserInput(source), QUrl::fromUserInput(dest), KIO::HideProgressInfo ); if( !job->exec() ) { return false; } return true; } else { QFile inputFile(source); QFile outputFile(dest); if (inputFile.open(QFile::ReadOnly) && outputFile.open(QFile::WriteOnly)) { QTextStream input(&inputFile); input.setCodec(QTextCodec::codecForName("UTF-8")); QTextStream output(&outputFile); output.setCodec(QTextCodec::codecForName("UTF-8")); while(!input.atEnd()) { QString line = input.readLine(); output << KMacroExpander::expandMacros(line, m_variables) << "\n"; } #ifndef Q_OS_WIN // Preserve file mode... QT_STATBUF statBuf; QT_FSTAT(inputFile.handle(), &statBuf); // Unix only, won't work in Windows, maybe KIO::chmod could be used ::fchmod(outputFile.handle(), statBuf.st_mode); #endif return true; } else { inputFile.close(); outputFile.close(); return false; } } } KDevelop::ContextMenuExtension AppWizardPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { Q_UNUSED(parent); KDevelop::ContextMenuExtension ext; if ( context->type() != KDevelop::Context::ProjectItemContext || !static_cast(context)->items().isEmpty() ) { return ext; } ext.addAction(KDevelop::ContextMenuExtension::ProjectGroup, m_newFromTemplate); return ext; } ProjectTemplatesModel* AppWizardPlugin::model() { if(!m_templatesModel) m_templatesModel = new ProjectTemplatesModel(this); return m_templatesModel; } QAbstractItemModel* AppWizardPlugin::templatesModel() { return model(); } QString AppWizardPlugin::knsConfigurationFile() const { return QStringLiteral("kdevappwizard.knsrc"); } QStringList AppWizardPlugin::supportedMimeTypes() const { const QStringList types{ QStringLiteral("application/x-desktop"), QStringLiteral("application/x-bzip-compressed-tar"), QStringLiteral("application/zip"), }; return types; } QIcon AppWizardPlugin::icon() const { return QIcon::fromTheme(QStringLiteral("project-development-new-template")); } QString AppWizardPlugin::name() const { return i18n("Project Templates"); } void AppWizardPlugin::loadTemplate(const QString& fileName) { model()->loadTemplateFile(fileName); } void AppWizardPlugin::reload() { model()->refresh(); } #include "appwizardplugin.moc" diff --git a/plugins/clang/CMakeLists.txt b/plugins/clang/CMakeLists.txt index f61d8d57d5..43b5ae339f 100644 --- a/plugins/clang/CMakeLists.txt +++ b/plugins/clang/CMakeLists.txt @@ -1,132 +1,133 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevclang\") add_definitions(${LLVM_CFLAGS}) include_directories(${CLANG_INCLUDE_DIRS}) set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_DL_LIBS}) check_cxx_source_compiles( "#include \nint main() { Dl_info info; return dladdr(nullptr, &info); }" HAVE_DLFCN) configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/libclang_include_path.h.cmake" "${CMAKE_CURRENT_BINARY_DIR}/libclang_include_path.h" @ONLY ) if(BUILD_TESTING) add_subdirectory(tests) endif() set(kdevclangprivate_SRCS clangsettings/clangsettingsmanager.cpp clangsettings/sessionsettings/sessionsettings.cpp codecompletion/completionhelper.cpp codecompletion/context.cpp codecompletion/includepathcompletioncontext.cpp codecompletion/model.cpp codegen/adaptsignatureaction.cpp codegen/adaptsignatureassistant.cpp codegen/codegenhelper.cpp codegen/clangrefactoring.cpp codegen/clangclasshelper.cpp codegen/sourcemanipulation.cpp duchain/builder.cpp duchain/clangdiagnosticevaluator.cpp duchain/clangducontext.cpp duchain/clanghelpers.cpp duchain/clangindex.cpp duchain/clangparsingenvironment.cpp duchain/clangparsingenvironmentfile.cpp duchain/clangpch.cpp duchain/clangproblem.cpp duchain/debugvisitor.cpp duchain/documentfinderhelpers.cpp duchain/duchainutils.cpp duchain/macrodefinition.cpp duchain/macronavigationcontext.cpp duchain/missingincludepathproblem.cpp duchain/navigationwidget.cpp duchain/parsesession.cpp duchain/todoextractor.cpp duchain/types/classspecializationtype.cpp duchain/unknowndeclarationproblem.cpp duchain/unsavedfile.cpp duchain/headerguardassistant.cpp util/clangdebug.cpp util/clangtypes.cpp util/clangutils.cpp ) # dummy call to add the data to KDevelopCategories # util/clangdebug.* cannot easily be generated with ecm_qt_declare_logging_category # as the current code does not use Q_DECLARE_LOGGING_CATEGORY but instead # has explicit code to tag KDEV_CLANG() as KDEVCLANGPRIVATE_EXPORT # Keep in sync with util/clangdebug.* declare_qt_logging_category(dummy_kdevclangprivate_SRCS TYPE PLUGIN HEADER dummy_debug.h IDENTIFIER KDEV_CLANG CATEGORY_BASENAME "clang" DESCRIPTION "clang-based language support" ) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) ki18n_wrap_ui(kdevclangprivate_SRCS clangsettings/sessionsettings/sessionsettings.ui ) kconfig_add_kcfg_files(kdevclangprivate_SRCS clangsettings/sessionsettings/sessionconfig.kcfgc) kdevelop_add_private_library(KDevClangPrivate SOURCES ${kdevclangprivate_SRCS}) target_link_libraries(KDevClangPrivate PUBLIC KDev::Language KDev::Project KDev::Util Clang::clang PRIVATE Qt5::Core KF5::TextEditor KF5::ThreadWeaver KDev::DefinesAndIncludesManager KDev::Util + KDev::Sublime ) if (HAVE_DLFCN) target_link_libraries(KDevClangPrivate PRIVATE ${CMAKE_DL_LIBS}) endif() install(DIRECTORY duchain/wrappedQtHeaders DESTINATION ${KDE_INSTALL_DATADIR}/kdevclangsupport DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_WRITE GROUP_EXECUTE WORLD_READ WORLD_EXECUTE FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ) set(kdevclangsupport_SRCS clangparsejob.cpp clangsupport.cpp clanghighlighting.cpp ) qt5_add_resources(kdevclangsupport_SRCS kdevclangsupport.qrc) kdevplatform_add_plugin(kdevclangsupport JSON kdevclangsupport.json SOURCES ${kdevclangsupport_SRCS}) target_link_libraries(kdevclangsupport KDevClangPrivate KF5::ThreadWeaver KF5::TextEditor KDev::Util KDev::Project KDev::DefinesAndIncludesManager ) install(FILES kdevclang.xml DESTINATION ${KDE_INSTALL_MIMEDIR}) update_xdg_mimetypes(${KDE_INSTALL_MIMEDIR}) diff --git a/plugins/clang/codegen/adaptsignatureaction.cpp b/plugins/clang/codegen/adaptsignatureaction.cpp index ce8c91af30..18de27060d 100644 --- a/plugins/clang/codegen/adaptsignatureaction.cpp +++ b/plugins/clang/codegen/adaptsignatureaction.cpp @@ -1,131 +1,136 @@ /* Copyright 2009 David Nolden 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 "adaptsignatureaction.h" #include "codegenhelper.h" #include "../duchain/duchainutils.h" #include "../util/clangdebug.h" #include #include #include #include #include #include +#include +#include +#include +// KF #include -#include using namespace KDevelop; AdaptSignatureAction::AdaptSignatureAction(const DeclarationId& definitionId, const ReferencedTopDUContext& definitionContext, const Signature& oldSignature, const Signature& newSignature, bool editingDefinition, const QList& renameActions) : m_otherSideId(definitionId) , m_otherSideTopContext(definitionContext) , m_oldSignature(oldSignature) , m_newSignature(newSignature) , m_editingDefinition(editingDefinition) , m_renameActions(renameActions) { } AdaptSignatureAction::~AdaptSignatureAction() { qDeleteAll(m_renameActions); } QString AdaptSignatureAction::description() const { return m_editingDefinition ? i18n("Update declaration signature") : i18n("Update definition signature"); } QString AdaptSignatureAction::toolTip() const { DUChainReadLocker lock; auto declaration = m_otherSideId.declaration(m_otherSideTopContext.data()); if (!declaration) { return {}; } KLocalizedString msg = m_editingDefinition ? ki18n("Update declaration signature\nfrom: %1\nto: %2") : ki18n("Update definition signature\nfrom: %1\nto: %2"); msg = msg.subs(CodegenHelper::makeSignatureString(declaration, m_oldSignature, m_editingDefinition)); msg = msg.subs(CodegenHelper::makeSignatureString(declaration, m_newSignature, !m_editingDefinition)); return msg.toString(); } void AdaptSignatureAction::execute() { ENSURE_CHAIN_NOT_LOCKED DUChainReadLocker lock; IndexedString url = m_otherSideTopContext->url(); lock.unlock(); m_otherSideTopContext = DUChain::self()->waitForUpdate(url, TopDUContext::AllDeclarationsContextsAndUses); if (!m_otherSideTopContext) { clangDebug() << "failed to update" << url.str(); return; } lock.lock(); Declaration* otherSide = m_otherSideId.declaration(m_otherSideTopContext.data()); if (!otherSide) { clangDebug() << "could not find definition"; return; } DUContext* functionContext = DUChainUtils::functionContext(otherSide); if (!functionContext) { clangDebug() << "no function context"; return; } if (!functionContext || functionContext->type() != DUContext::Function) { clangDebug() << "no correct function context"; return; } DocumentChangeSet changes; KTextEditor::Range parameterRange = ClangIntegration::DUChainUtils::functionSignatureRange(otherSide); QString newText = CodegenHelper::makeSignatureString(otherSide, m_newSignature, !m_editingDefinition); if (!m_editingDefinition) { // append a newline after the method signature in case the method definition follows newText += QLatin1Char('\n'); } DocumentChange changeParameters(functionContext->url(), parameterRange, QString(), newText); lock.unlock(); changeParameters.m_ignoreOldText = true; changes.addChange(changeParameters); changes.setReplacementPolicy(DocumentChangeSet::WarnOnFailedChange); DocumentChangeSet::ChangeResult result = changes.applyAllChanges(); if (!result) { - KMessageBox::error(nullptr, i18n("Failed to apply changes: %1", result.m_failureReason)); + const QString messageText = i18n("Failed to apply changes: %1", result.m_failureReason); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); } emit executed(this); for (RenameAction* renAct : m_renameActions) { renAct->execute(); } } #include "moc_adaptsignatureaction.cpp" diff --git a/plugins/clang/codegen/clangrefactoring.cpp b/plugins/clang/codegen/clangrefactoring.cpp index 3f52814719..d53e78f578 100644 --- a/plugins/clang/codegen/clangrefactoring.cpp +++ b/plugins/clang/codegen/clangrefactoring.cpp @@ -1,290 +1,293 @@ /* * This file is part of KDevelop * * Copyright 2015 Sergey Kalinichev * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "clangrefactoring.h" #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include +#include #include "duchain/clanghelpers.h" #include "duchain/documentfinderhelpers.h" #include "duchain/duchainutils.h" #include "sourcemanipulation.h" #include "util/clangdebug.h" using namespace KDevelop; namespace { bool isDestructor(Declaration* decl) { if (auto functionDef = dynamic_cast(decl)) { // we found a definition, e.g. "Foo::~Foo()" const auto functionDecl = functionDef->declaration(decl->topContext()); if (auto classFunctionDecl = dynamic_cast(functionDecl)) { return classFunctionDecl->isDestructor(); } } else if (auto classFunctionDecl = dynamic_cast(decl)) { // we found a declaration, e.g. "~Foo()" return classFunctionDecl->isDestructor(); } return false; } } ClangRefactoring::ClangRefactoring(QObject* parent) : BasicRefactoring(parent) { qRegisterMetaType(); } void ClangRefactoring::fillContextMenu(ContextMenuExtension& extension, Context* context, QWidget* parent) { auto declContext = dynamic_cast(context); if (!declContext) { return; } DUChainReadLocker lock; auto declaration = declContext->declaration().data(); if (!declaration) { return; } QFileInfo fileInfo(declaration->topContext()->url().str()); if (!fileInfo.isWritable()) { return; } auto action = new QAction(i18n("Rename %1", declaration->qualifiedIdentifier().toString()), parent); action->setData(QVariant::fromValue(IndexedDeclaration(declaration))); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); connect(action, &QAction::triggered, this, &ClangRefactoring::executeRenameAction); extension.addAction(ContextMenuExtension::RefactorGroup, action); if (!validCandidateToMoveIntoSource(declaration)) { return; } action = new QAction( i18n("Create separate definition for %1", declaration->qualifiedIdentifier().toString()), parent); action->setData(QVariant::fromValue(IndexedDeclaration(declaration))); connect(action, &QAction::triggered, this, &ClangRefactoring::executeMoveIntoSourceAction); extension.addAction(ContextMenuExtension::RefactorGroup, action); } bool ClangRefactoring::validCandidateToMoveIntoSource(Declaration* decl) { if (!decl || !decl->isFunctionDeclaration() || !decl->type()) { return false; } if (!decl->internalContext() || decl->internalContext()->type() != DUContext::Function) { return false; } if (!decl->isDefinition()) { return false; } auto childCtx = decl->internalContext()->childContexts(); if (childCtx.isEmpty()) { return false; } auto ctx = childCtx.first(); if (!ctx || ctx->type() != DUContext::Other) { return false; } auto functionDecl = dynamic_cast(decl); if (!functionDecl || functionDecl->isInline()) { return false; } return true; } QString ClangRefactoring::moveIntoSource(const IndexedDeclaration& iDecl) { DUChainReadLocker lock; auto decl = iDecl.data(); if (!decl) { return i18n("No declaration under cursor"); } const auto headerUrl = decl->url(); auto targetUrl = DocumentFinderHelpers::sourceForHeader(headerUrl.str()); if (targetUrl.isEmpty() || targetUrl == headerUrl.str()) { // TODO: Create source file if it doesn't exist return i18n("No source file available for %1.", headerUrl.str()); } lock.unlock(); const IndexedString indexedTargetUrl(targetUrl); auto top = DUChain::self()->waitForUpdate(headerUrl, KDevelop::TopDUContext::AllDeclarationsAndContexts); auto targetTopContext = DUChain::self()->waitForUpdate(indexedTargetUrl, KDevelop::TopDUContext::AllDeclarationsAndContexts); lock.lock(); if (!targetTopContext) { return i18n("Failed to update DUChain for %1.", targetUrl); } if (!top || !iDecl.data() || iDecl.data() != decl) { return i18n("Declaration lost while updating."); } clangDebug() << "moving" << decl->qualifiedIdentifier(); if (!validCandidateToMoveIntoSource(decl)) { return i18n("Cannot create definition for this declaration."); } auto otherCtx = decl->internalContext()->childContexts().first(); auto code = createCodeRepresentation(headerUrl); if (!code) { return i18n("No document for %1", headerUrl.str()); } auto bodyRange = otherCtx->rangeInCurrentRevision(); auto prefixRange(ClangIntegration::DUChainUtils::functionSignatureRange(decl)); const auto prefixText = code->rangeText(prefixRange); for (int i = prefixText.length() - 1; i >= 0 && prefixText.at(i).isSpace(); --i) { if (bodyRange.start().column() == 0) { bodyRange.setStart(bodyRange.start() - KTextEditor::Cursor(1, 0)); if (bodyRange.start().line() == prefixRange.start().line()) { bodyRange.setStart(KTextEditor::Cursor(bodyRange.start().line(), prefixRange.start().column() + i)); } else { int lastNewline = prefixText.lastIndexOf(QLatin1Char('\n'), i - 1); bodyRange.setStart(KTextEditor::Cursor(bodyRange.start().line(), i - lastNewline - 1)); } } else { bodyRange.setStart(bodyRange.start() - KTextEditor::Cursor(0, 1)); } } const QString body = code->rangeText(bodyRange); SourceCodeInsertion ins(targetTopContext); auto parentId = decl->internalContext()->parentContext()->scopeIdentifier(false); ins.setSubScope(parentId); Identifier id(IndexedString(decl->qualifiedIdentifier().mid(parentId.count()).toString())); clangDebug() << "id:" << id; if (!ins.insertFunctionDeclaration(decl, id, body)) { return i18n("Insertion failed"); } lock.unlock(); auto applied = ins.changes().applyAllChanges(); if (!applied) { return i18n("Applying changes failed: %1", applied.m_failureReason); } // replace function body with a semicolon DocumentChangeSet changeHeader; changeHeader.addChange(DocumentChange(headerUrl, bodyRange, body, QStringLiteral(";"))); applied = changeHeader.applyAllChanges(); if (!applied) { return i18n("Applying changes failed: %1", applied.m_failureReason); } ICore::self()->languageController()->backgroundParser()->addDocument(headerUrl); ICore::self()->languageController()->backgroundParser()->addDocument(indexedTargetUrl); return {}; } void ClangRefactoring::executeMoveIntoSourceAction() { auto action = qobject_cast(sender()); Q_ASSERT(action); auto iDecl = action->data().value(); if (!iDecl.isValid()) { iDecl = declarationUnderCursor(false); } const auto error = moveIntoSource(iDecl); if (!error.isEmpty()) { - KMessageBox::error(nullptr, error); + auto* message = new Sublime::Message(error, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); } } DocumentChangeSet::ChangeResult ClangRefactoring::applyChangesToDeclarations(const QString& oldName, const QString& newName, DocumentChangeSet& changes, const QList& declarations) { for (const IndexedDeclaration decl : declarations) { Declaration *declaration = decl.data(); if (!declaration) continue; if (declaration->range().isEmpty()) clangDebug() << "found empty declaration:" << declaration->toString(); // special handling for dtors, their name is not "Foo", but "~Foo" // see https://bugs.kde.org/show_bug.cgi?id=373452 QString fixedOldName = oldName; QString fixedNewName = newName; if (isDestructor(declaration)) { clangDebug() << "found destructor:" << declaration->toString() << "-- making sure we replace the identifier correctly"; fixedOldName = QLatin1Char('~') + oldName; fixedNewName = QLatin1Char('~') + newName; } TopDUContext *top = declaration->topContext(); DocumentChangeSet::ChangeResult result = changes.addChange(DocumentChange(top->url(), declaration->rangeInCurrentRevision(), fixedOldName, fixedNewName)); if (!result) return result; } return KDevelop::DocumentChangeSet::ChangeResult::successfulResult(); } diff --git a/plugins/clangtidy/job.cpp b/plugins/clangtidy/job.cpp index 66cdc370e8..a0860398c4 100644 --- a/plugins/clangtidy/job.cpp +++ b/plugins/clangtidy/job.cpp @@ -1,199 +1,203 @@ /* * This file is part of KDevelop * * Copyright 2016 Carlos Nihelton * Copyright 2018 Friedrich W. H. Kossebau * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "job.h" +// KDevPlatform +#include +#include +#include // KF #include -#include // Qt #include #include #include namespace ClangTidy { // uses ' for quoting QString inlineYaml(const Job::Parameters& parameters) { QString result; result.append(QLatin1String("{Checks: '") + parameters.enabledChecks + QLatin1Char('\'')); if (!parameters.headerFilter.isEmpty()) { // TODO: the regex might need escpaing for potential quotes of all kinds result.append(QLatin1String(", HeaderFilterRegex: '") + parameters.headerFilter + QLatin1Char('\'')); } result.append(QLatin1Char('}')); return result; } // uses " for quoting QStringList commandLineArgs(const Job::Parameters& parameters) { QStringList args{ parameters.executablePath, QLatin1String("-p=\"") + parameters.buildDir + QLatin1Char('\"'), // don't add statistics we are not interested in to parse anyway QStringLiteral("-quiet"), }; if (!parameters.additionalParameters.isEmpty()) { args << parameters.additionalParameters; } if (parameters.checkSystemHeaders) { args << QStringLiteral("--system-headers"); } if (!parameters.useConfigFile) { args << QLatin1String("--config=\"") + inlineYaml(parameters) + QLatin1Char('\"'); } return args; } Job::Job(const Parameters& params, QObject* parent) : KDevelop::CompileAnalyzeJob(parent) , m_parameters(params) { setJobName(i18n("Clang-Tidy Analysis")); setParallelJobCount(m_parameters.parallelJobCount); setBuildDirectoryRoot(m_parameters.buildDir); const auto commandLine = commandLineArgs(m_parameters); setCommand(commandLine.join(QLatin1Char(' ')), false); setToolDisplayName(QStringLiteral("Clang-Tidy")); setSources(m_parameters.filePaths); connect(&m_parser, &ClangTidyParser::problemsDetected, this, &Job::problemsDetected); qCDebug(KDEV_CLANGTIDY) << "checking files" << params.filePaths; } Job::~Job() { } void Job::processStdoutLines(const QStringList& lines) { m_parser.addData(lines); m_standardOutput << lines; } void Job::processStderrLines(const QStringList& lines) { static const auto xmlStartRegex = QRegularExpression(QStringLiteral("\\s*<")); for (const QString& line : lines) { // unfortunately sometime clangtidy send non-XML messages to stderr. // For example, if we pass '-I /missing_include_dir' to the argument list, // then stderr output will contains such line (tested on clangtidy 1.72): // // (information) Couldn't find path given by -I '/missing_include_dir' // // Therefore we must 'move' such messages to m_standardOutput. if (line.indexOf(xmlStartRegex) != -1) { // the line contains XML m_xmlOutput << line; } else { m_standardOutput << line; } } } void Job::postProcessStdout(const QStringList& lines) { processStdoutLines(lines); KDevelop::CompileAnalyzeJob::postProcessStdout(lines); } void Job::postProcessStderr(const QStringList& lines) { processStderrLines(lines); KDevelop::CompileAnalyzeJob::postProcessStderr(lines); } void Job::start() { m_standardOutput.clear(); m_xmlOutput.clear(); KDevelop::CompileAnalyzeJob::start(); } void Job::childProcessError(QProcess::ProcessError processError) { - QString message; + QString messageText; switch (processError) { case QProcess::FailedToStart: { - message = i18n("Failed to start Clang-Tidy process."); + messageText = i18n("Failed to start Clang-Tidy process."); break; } case QProcess::Crashed: - message = i18n("Clang-tidy crashed."); + messageText = i18n("Clang-tidy crashed."); break; case QProcess::Timedout: - message = i18n("Clang-tidy process timed out."); + messageText = i18n("Clang-tidy process timed out."); break; case QProcess::WriteError: - message = i18n("Write to Clang-tidy process failed."); + messageText = i18n("Write to Clang-tidy process failed."); break; case QProcess::ReadError: - message = i18n("Read from Clang-tidy process failed."); + messageText = i18n("Read from Clang-tidy process failed."); break; case QProcess::UnknownError: // current clangtidy errors will be displayed in the output view // don't notify the user break; } - if (!message.isEmpty()) { - KMessageBox::error(qApp->activeWindow(), message, i18n("Clang-tidy Error")); + if (!messageText.isEmpty()) { + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); } KDevelop::CompileAnalyzeJob::childProcessError(processError); } void Job::childProcessExited(int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode != 0) { qCDebug(KDEV_CLANGTIDY) << "clang-tidy failed, standard output: "; qCDebug(KDEV_CLANGTIDY) << m_standardOutput.join(QLatin1Char('\n')); qCDebug(KDEV_CLANGTIDY) << "clang-tidy failed, XML output: "; qCDebug(KDEV_CLANGTIDY) << m_xmlOutput.join(QLatin1Char('\n')); } KDevelop::CompileAnalyzeJob::childProcessExited(exitCode, exitStatus); } } // namespace ClangTidy diff --git a/plugins/cmake/CMakeLists.txt b/plugins/cmake/CMakeLists.txt index f05677c59b..fc474bca8e 100644 --- a/plugins/cmake/CMakeLists.txt +++ b/plugins/cmake/CMakeLists.txt @@ -1,135 +1,137 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevcmake\") include_directories(${CMAKE_CURRENT_SOURCE_DIR}/parser) if(BUILD_TESTING) add_subdirectory(tests) endif() add_subdirectory(icons) # enable this if you want to have the cmake debug visitor run on each CMakeLists.txt # the debug visitor prints out the Ast for the CMakeLists.txt file. #add_definitions( -DCMAKEDEBUGVISITOR ) declare_qt_logging_category(cmake_LOG_SRCS TYPE PLUGIN IDENTIFIER CMAKE CATEGORY_BASENAME "cmake" ) set( cmakecommon_SRCS parser/cmListFileLexer.c parser/cmakecachereader.cpp parser/cmakelistsparser.cpp parser/cmakeduchaintypes.cpp cmakeutils.cpp cmakeextraargumentshistory.cpp cmakebuilddirchooser.cpp cmakeserver.cpp ${cmake_LOG_SRCS} ) set_source_files_properties(parser/cmListFileLexer.c PROPERTIES COMPILE_FLAGS "-DYY_NO_INPUT -DYY_NO_UNPUT") set( cmakecommon_UI cmakebuilddirchooser.ui ) set( cmakemanager_SRCS testing/ctestutils.cpp testing/ctestfindjob.cpp testing/ctestrunjob.cpp testing/ctestsuite.cpp testing/qttestdelegate.cpp cmakeimportjsonjob.cpp cmakeserverimportjob.cpp cmakenavigationwidget.cpp cmakemanager.cpp cmakeprojectdata.cpp cmakemodelitems.cpp duchain/cmakeparsejob.cpp duchain/usebuilder.cpp duchain/declarationbuilder.cpp duchain/contextbuilder.cpp cmakecodecompletionmodel.cpp # cmakecommitchangesjob.cpp # cmakeedit.cpp ${cmake_LOG_SRCS} ) set( cmakemanager_UI cmakepossibleroots.ui ) set( cmakesettings_SRCS settings/cmakepreferences.cpp settings/cmakecachemodel.cpp settings/cmakecachedelegate.cpp settings/cmakecachemodel.cpp ) ki18n_wrap_ui(cmakesettings_SRCS settings/cmakebuildsettings.ui) set( cmakedoc_SRCS cmakedocumentation.cpp cmakehelpdocumentation.cpp cmakecommandscontents.cpp ) if(MSVC) add_definitions(-DYY_NO_UNISTD_H) endif() # Note: This library doesn't follow API/ABI/BC rules and shouldn't have a SOVERSION # Its only purpose is to support the plugin without needing to add all source files # to the plugin target kconfig_add_kcfg_files( cmakecommon_SRCS cmakebuilderconfig.kcfgc ) ki18n_wrap_ui( cmakecommon_SRCS ${cmakecommon_UI} ) kdevelop_add_private_library(KDevCMakeCommon SOURCES ${cmakecommon_SRCS}) target_link_libraries(KDevCMakeCommon PUBLIC KDev::Interfaces KDev::Project KDev::Util KDev::Language KF5::TextEditor ) ki18n_wrap_ui( cmakemanager_SRCS ${cmakemanager_UI} ) add_library( kdevcmakemanagernosettings STATIC ${cmakemanager_SRCS}) target_link_libraries(kdevcmakemanagernosettings KDevCMakeCommon kdevmakefileresolver KDev::Util KDev::Interfaces KDev::Project KDev::Language + KDev::Sublime KDev::OutputView KF5::KIOWidgets KF5::TextEditor Qt5::Concurrent ) kdevplatform_add_plugin(kdevcmakemanager JSON kdevcmakemanager.json SOURCES ${cmakemanager_SRCS} ${cmakesettings_SRCS}) target_link_libraries(kdevcmakemanager KDevCMakeCommon kdevmakefileresolver KDev::Util KDev::Interfaces KDev::Project KDev::Language KDev::Shell KDev::OutputView KF5::KIOWidgets KF5::TextEditor Qt5::Concurrent ) kdevplatform_add_plugin(kdevcmakedocumentation JSON kdevcmakedocumentation.json SOURCES ${cmakedoc_SRCS}) target_link_libraries( kdevcmakedocumentation KDevCMakeCommon KDev::Interfaces KDev::Project KDev::Language KDev::Documentation + KDev::Sublime KF5::ItemModels KF5::TextEditor ) diff --git a/plugins/cmake/cmakemanager.cpp b/plugins/cmake/cmakemanager.cpp index be419a9c4c..bdd35832f3 100644 --- a/plugins/cmake/cmakemanager.cpp +++ b/plugins/cmake/cmakemanager.cpp @@ -1,1082 +1,1086 @@ /* KDevelop CMake Support * * Copyright 2006 Matt Rogers * Copyright 2007-2013 Aleix Pol * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "cmakemanager.h" #include "cmakeedit.h" #include "cmakeutils.h" #include "cmakeprojectdata.h" #include "duchain/cmakeparsejob.h" #include "cmakeimportjsonjob.h" #include "debug.h" #include "settings/cmakepreferences.h" #include "cmakecodecompletionmodel.h" #include "cmakenavigationwidget.h" #include "icmakedocumentation.h" #include "cmakemodelitems.h" #include "testing/ctestutils.h" #include "cmakeserverimportjob.h" #include "cmakeserver.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include Q_DECLARE_METATYPE(KDevelop::IProject*) using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(CMakeSupportFactory, "kdevcmakemanager.json", registerPlugin(); ) const QString DIALOG_CAPTION = i18n("KDevelop - CMake Support"); CMakeManager::CMakeManager( QObject* parent, const QVariantList& ) : KDevelop::AbstractFileManagerPlugin( QStringLiteral("kdevcmakemanager"), parent ) , m_filter( new ProjectFilterManager( this ) ) { if (CMake::findExecutable().isEmpty()) { setErrorDescription(i18n("Unable to find a CMake executable. Is one installed on the system?")); m_highlight = nullptr; return; } m_highlight = new KDevelop::CodeHighlighting(this); new CodeCompletion(this, new CMakeCodeCompletionModel(this), name()); connect(ICore::self()->projectController(), &IProjectController::projectClosing, this, &CMakeManager::projectClosing); connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &CMakeManager::reloadProjects); connect(this, &KDevelop::AbstractFileManagerPlugin::folderAdded, this, &CMakeManager::folderAdded); // m_fileSystemChangeTimer = new QTimer(this); // m_fileSystemChangeTimer->setSingleShot(true); // m_fileSystemChangeTimer->setInterval(100); // connect(m_fileSystemChangeTimer,SIGNAL(timeout()),SLOT(filesystemBuffererTimeout())); } CMakeManager::~CMakeManager() { parseLock()->lockForWrite(); // By locking the parse-mutexes, we make sure that parse jobs get a chance to finish in a good state parseLock()->unlock(); } bool CMakeManager::hasBuildInfo(ProjectBaseItem* item) const { return m_projects[item->project()].compilationData.files.contains(item->path()); } Path CMakeManager::buildDirectory(KDevelop::ProjectBaseItem *item) const { // CMakeFolderItem *fi=dynamic_cast(item); // Path ret; // ProjectBaseItem* parent = fi ? fi->formerParent() : item->parent(); // if (parent) // ret=buildDirectory(parent); // else // ret=Path(CMake::currentBuildDir(item->project())); // // if(fi) // ret.addPath(fi->buildDir()); // return ret; return Path(CMake::currentBuildDir(item->project())); } KDevelop::ProjectFolderItem* CMakeManager::import( KDevelop::IProject *project ) { CMake::checkForNeedingConfigure(project); return AbstractFileManagerPlugin::import(project); } class ChooseCMakeInterfaceJob : public ExecuteCompositeJob { Q_OBJECT public: ChooseCMakeInterfaceJob(IProject* project, CMakeManager* manager) : ExecuteCompositeJob(manager, {}) , project(project) , manager(manager) { } void start() override { server.reset(new CMakeServer(project)); connect(server.data(), &CMakeServer::connected, this, &ChooseCMakeInterfaceJob::successfulConnection); connect(server.data(), &CMakeServer::finished, this, &ChooseCMakeInterfaceJob::failedConnection); } private: void successfulConnection() { auto job = new CMakeServerImportJob(project, server, this); connect(job, &CMakeServerImportJob::result, this, [this, job](){ if (job->error() == 0) { manager->integrateData(job->projectData(), job->project()); } }); addSubjob(job); ExecuteCompositeJob::start(); } void failedConnection(int code) { Q_ASSERT(code > 0); Q_ASSERT(!server->isServerAvailable()); qCDebug(CMAKE) << "CMake does not provide server mode, using compile_commands.json to import" << project->name(); // parse the JSON file auto* job = new CMakeImportJsonJob(project, this); // create the JSON file if it doesn't exist auto commandsFile = CMake::commandsFile(project); if (!QFileInfo::exists(commandsFile.toLocalFile())) { qCDebug(CMAKE) << "couldn't find commands file:" << commandsFile << "- now trying to reconfigure"; addSubjob(manager->builder()->configure(project)); } connect(job, &CMakeImportJsonJob::result, this, [this, job]() { if (job->error() == 0) { manager->integrateData(job->projectData(), job->project()); } }); addSubjob(job); ExecuteCompositeJob::start(); } QSharedPointer server; IProject* const project; CMakeManager* const manager; }; KJob* CMakeManager::createImportJob(ProjectFolderItem* item) { auto project = item->project(); auto job = new ChooseCMakeInterfaceJob(project, this); connect(job, &KJob::result, this, [this, job, project](){ if (job->error() != 0) { qCWarning(CMAKE) << "couldn't load project successfully" << project->name(); m_projects.remove(project); showConfigureErrorMessage(project->name(), job->errorText()); } }); const QList jobs = { job, KDevelop::AbstractFileManagerPlugin::createImportJob(item) // generate the file system listing }; Q_ASSERT(!jobs.contains(nullptr)); auto* composite = new ExecuteCompositeJob(this, jobs); // even if the cmake call failed, we want to load the project so that the project can be worked on composite->setAbortOnError(false); return composite; } // QList CMakeManager::parse(ProjectFolderItem*) // { return QList< ProjectFolderItem* >(); } // // QList CMakeManager::targets() const { QList ret; for (auto it = m_projects.begin(), end = m_projects.end(); it != end; ++it) { IProject* p = it.key(); ret+=p->projectItem()->targetList(); } return ret; } CMakeFile CMakeManager::fileInformation(KDevelop::ProjectBaseItem* item) const { const auto & data = m_projects[item->project()].compilationData; QHash::const_iterator it = data.files.constFind(item->path()); if (it == data.files.constEnd()) { // if the item path contains a symlink, then we will not find it in the lookup table // as that only only stores canonicalized paths. Thus, we fallback to // to the canonicalized path and see if that brings up any matches const auto canonicalized = Path(QFileInfo(item->path().toLocalFile()).canonicalFilePath()); it = data.files.constFind(canonicalized); } if (it != data.files.constEnd()) { return *it; } else { // otherwise look for siblings and use the include paths of any we find const Path folder = item->folder() ? item->path() : item->path().parent(); for( it = data.files.constBegin(); it != data.files.constEnd(); ++it) { if (folder.isDirectParentOf(it.key())) { return *it; } } } // last-resort fallback: bubble up the parent chain, and keep looking for include paths if (auto parent = item->parent()) { return fileInformation(parent); } return {}; } Path::List CMakeManager::includeDirectories(KDevelop::ProjectBaseItem *item) const { return fileInformation(item).includes; } Path::List CMakeManager::frameworkDirectories(KDevelop::ProjectBaseItem *item) const { return fileInformation(item).frameworkDirectories; } QHash CMakeManager::defines(KDevelop::ProjectBaseItem *item ) const { return fileInformation(item).defines; } QString CMakeManager::extraArguments(KDevelop::ProjectBaseItem *item) const { return fileInformation(item).compileFlags; } KDevelop::IProjectBuilder * CMakeManager::builder() const { IPlugin* i = core()->pluginController()->pluginForExtension( QStringLiteral("org.kdevelop.IProjectBuilder"), QStringLiteral("KDevCMakeBuilder")); Q_ASSERT(i); auto* _builder = i->extension(); Q_ASSERT(_builder ); return _builder ; } bool CMakeManager::reload(KDevelop::ProjectFolderItem* folder) { qCDebug(CMAKE) << "reloading" << folder->path(); IProject* project = folder->project(); if (!project->isReady()) return false; KJob *job = createImportJob(folder); project->setReloadJob(job); ICore::self()->runController()->registerJob( job ); if (folder == project->projectItem()) { connect(job, &KJob::finished, this, [project](KJob* job) { if (job->error()) return; emit KDevelop::ICore::self()->projectController()->projectConfigurationChanged(project); KDevelop::ICore::self()->projectController()->reparseProject(project, true); }); } return true; } static void populateTargets(ProjectFolderItem* folder, const QHash>& targets) { static QSet standardTargets = { QStringLiteral("edit_cache"), QStringLiteral("rebuild_cache"), QStringLiteral("list_install_components"), QStringLiteral("test"), //not really standard, but applicable for make and ninja QStringLiteral("install") }; QList dirTargets = kFilter>(targets[folder->path()], [](const CMakeTarget& target) -> bool { return target.type != CMakeTarget::Custom || (!target.name.endsWith(QLatin1String("_automoc")) && !target.name.endsWith(QLatin1String("_autogen")) && !standardTargets.contains(target.name) && !target.name.startsWith(QLatin1String("install/")) ); }); const auto tl = folder->targetList(); for (ProjectTargetItem* item : tl) { const auto idx = kIndexOf(dirTargets, [item](const CMakeTarget& target) { return target.name == item->text(); }); if (idx < 0) { delete item; } else { auto cmakeItem = dynamic_cast(item); if (cmakeItem) cmakeItem->setBuiltUrl(dirTargets[idx].artifacts.value(0)); dirTargets.removeAt(idx); } } for (const auto& target : qAsConst(dirTargets)) { switch(target.type) { case CMakeTarget::Executable: new CMakeTargetItem(folder, target.name, target.artifacts.value(0)); break; case CMakeTarget::Library: new ProjectLibraryTargetItem(folder->project(), target.name, folder); break; case CMakeTarget::Custom: new ProjectTargetItem(folder->project(), target.name, folder); break; } } const auto folderItems = folder->folderList(); for (ProjectFolderItem* children : folderItems) { populateTargets(children, targets); } } void CMakeManager::integrateData(const CMakeProjectData &data, KDevelop::IProject* project) { if (data.m_server) { connect(data.m_server.data(), &CMakeServer::response, project, [this, project](const QJsonObject& response) { serverResponse(project, response); }); } else { connect(data.watcher.data(), &QFileSystemWatcher::fileChanged, this, &CMakeManager::dirtyFile); connect(data.watcher.data(), &QFileSystemWatcher::directoryChanged, this, &CMakeManager::dirtyFile); } m_projects[project] = data; populateTargets(project->projectItem(), data.targets); CTestUtils::createTestSuites(data.m_testSuites, data.targets, project); } void CMakeManager::serverResponse(KDevelop::IProject* project, const QJsonObject& response) { if (response[QStringLiteral("type")] == QLatin1String("signal")) { if (response[QStringLiteral("name")] == QLatin1String("dirty")) { m_projects[project].m_server->configure({}); } else qCDebug(CMAKE) << "unhandled signal response..." << project << response; } else if (response[QStringLiteral("type")] == QLatin1String("error")) { showConfigureErrorMessage(project->name(), response[QStringLiteral("errorMessage")].toString()); } else if (response[QStringLiteral("type")] == QLatin1String("reply")) { const auto inReplyTo = response[QStringLiteral("inReplyTo")]; if (inReplyTo == QLatin1String("configure")) { m_projects[project].m_server->compute(); } else if (inReplyTo == QLatin1String("compute")) { m_projects[project].m_server->codemodel(); } else if(inReplyTo == QLatin1String("codemodel")) { auto &data = m_projects[project]; CMakeServerImportJob::processCodeModel(response, data); populateTargets(project->projectItem(), data.targets); } else { qCDebug(CMAKE) << "unhandled reply response..." << project << response; } } else { qCDebug(CMAKE) << "unhandled response..." << project << response; } } // void CMakeManager::deletedWatchedDirectory(IProject* p, const QUrl &dir) // { // if(p->folder().equals(dir, QUrl::CompareWithoutTrailingSlash)) { // ICore::self()->projectController()->closeProject(p); // } else { // if(dir.fileName()=="CMakeLists.txt") { // QList folders = p->foldersForUrl(dir.upUrl()); // foreach(ProjectFolderItem* folder, folders) // reload(folder); // } else { // qDeleteAll(p->itemsForUrl(dir)); // } // } // } // void CMakeManager::directoryChanged(const QString& dir) // { // m_fileSystemChangedBuffer << dir; // m_fileSystemChangeTimer->start(); // } // void CMakeManager::filesystemBuffererTimeout() // { // Q_FOREACH(const QString& file, m_fileSystemChangedBuffer) { // realDirectoryChanged(file); // } // m_fileSystemChangedBuffer.clear(); // } // void CMakeManager::realDirectoryChanged(const QString& dir) // { // QUrl path(dir); // IProject* p=ICore::self()->projectController()->findProjectForUrl(dir); // if(!p || !p->isReady()) { // if(p) { // m_fileSystemChangedBuffer << dir; // m_fileSystemChangeTimer->start(); // } // return; // } // // if(!QFile::exists(dir)) { // path.adjustPath(QUrl::AddTrailingSlash); // deletedWatchedDirectory(p, path); // } else // dirtyFile(dir); // } QList< KDevelop::ProjectTargetItem * > CMakeManager::targets(KDevelop::ProjectFolderItem * folder) const { return folder->targetList(); } QString CMakeManager::name() const { return languageName().str(); } IndexedString CMakeManager::languageName() { static IndexedString name("CMake"); return name; } KDevelop::ParseJob * CMakeManager::createParseJob(const IndexedString &url) { return new CMakeParseJob(url, this); } KDevelop::ICodeHighlighting* CMakeManager::codeHighlighting() const { return m_highlight; } // ContextMenuExtension CMakeManager::contextMenuExtension( KDevelop::Context* context ) // { // if( context->type() != KDevelop::Context::ProjectItemContext ) // return IPlugin::contextMenuExtension( context ); // // KDevelop::ProjectItemContext* ctx = dynamic_cast( context ); // QList items = ctx->items(); // // if( items.isEmpty() ) // return IPlugin::contextMenuExtension( context ); // // m_clickedItems = items; // ContextMenuExtension menuExt; // if(items.count()==1 && dynamic_cast(items.first())) // { // QAction * action = new QAction( i18n( "Jump to Target Definition" ), this ); // connect( action, SIGNAL(triggered()), this, SLOT(jumpToDeclaration()) ); // menuExt.addAction( ContextMenuExtension::ProjectGroup, action ); // } // // return menuExt; // } // // void CMakeManager::jumpToDeclaration() // { // DUChainAttatched* du=dynamic_cast(m_clickedItems.first()); // if(du) // { // KTextEditor::Cursor c; // QUrl url; // { // KDevelop::DUChainReadLocker lock; // Declaration* decl = du->declaration().data(); // if(!decl) // return; // c = decl->rangeInCurrentRevision().start(); // url = decl->url().toUrl(); // } // // ICore::self()->documentController()->openDocument(url, c); // } // } // // // TODO: Port to Path API // bool CMakeManager::moveFilesAndFolders(const QList< ProjectBaseItem* > &items, ProjectFolderItem* toFolder) // { // using namespace CMakeEdit; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Move files and folders within CMakeLists as follows:")); // // bool cmakeSuccessful = true; // CMakeFolderItem *nearestCMakeFolderItem = nearestCMakeFolder(toFolder); // IProject* project=toFolder->project(); // // QList movedUrls; // QList oldUrls; // foreach(ProjectBaseItem *movedItem, items) // { // QList dirtyItems = cmakeListedItemsAffectedByUrlChange(project, movedItem->url()); // QUrl movedItemNewUrl = toFolder->url(); // movedItemNewUrl.addPath(movedItem->baseName()); // if (movedItem->folder()) // movedItemNewUrl.adjustPath(QUrl::AddTrailingSlash); // foreach(ProjectBaseItem* dirtyItem, dirtyItems) // { // QUrl dirtyItemNewUrl = afterMoveUrl(dirtyItem->url(), movedItem->url(), movedItemNewUrl); // if (CMakeFolderItem* folder = dynamic_cast(dirtyItem)) // { // cmakeSuccessful &= changesWidgetRemoveCMakeFolder(folder, &changesWidget); // cmakeSuccessful &= changesWidgetAddFolder(dirtyItemNewUrl, nearestCMakeFolderItem, &changesWidget); // } // else if (dirtyItem->parent()->target()) // { // cmakeSuccessful &= changesWidgetMoveTargetFile(dirtyItem, dirtyItemNewUrl, &changesWidget); // } // } // // oldUrls += movedItem->url(); // movedUrls += movedItemNewUrl; // } // // if (changesWidget.hasDocuments() && cmakeSuccessful) // cmakeSuccessful &= changesWidget.exec() && changesWidget.applyAllChanges(); // // if (!cmakeSuccessful) // { // if (KMessageBox::questionYesNo( QApplication::activeWindow(), // i18n("Changes to CMakeLists failed, abort move?"), // DIALOG_CAPTION ) == KMessageBox::Yes) // return false; // } // // QList::const_iterator it1=oldUrls.constBegin(), it1End=oldUrls.constEnd(); // QList::const_iterator it2=movedUrls.constBegin(); // Q_ASSERT(oldUrls.size()==movedUrls.size()); // for(; it1!=it1End; ++it1, ++it2) // { // if (!KDevelop::renameUrl(project, *it1, *it2)) // return false; // // QList renamedItems = project->itemsForUrl(*it2); // bool dir = QFileInfo(it2->toLocalFile()).isDir(); // foreach(ProjectBaseItem* item, renamedItems) { // if(dir) // emit folderRenamed(Path(*it1), item->folder()); // else // emit fileRenamed(Path(*it1), item->file()); // } // } // // return true; // } // // bool CMakeManager::copyFilesAndFolders(const KDevelop::Path::List &items, KDevelop::ProjectFolderItem* toFolder) // { // IProject* project = toFolder->project(); // foreach(const Path& path, items) { // if (!KDevelop::copyUrl(project, path.toUrl(), toFolder->url())) // return false; // } // // return true; // } // // bool CMakeManager::removeFilesAndFolders(const QList &items) // { // using namespace CMakeEdit; // // IProject* p = 0; // QList urls; // foreach(ProjectBaseItem* item, items) // { // Q_ASSERT(item->folder() || item->file()); // // urls += item->url(); // if(!p) // p = item->project(); // } // // //First do CMakeLists changes // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Remove files and folders from CMakeLists as follows:")); // // bool cmakeSuccessful = changesWidgetRemoveItems(cmakeListedItemsAffectedByItemsChanged(items).toSet(), &changesWidget); // // if (changesWidget.hasDocuments() && cmakeSuccessful) // cmakeSuccessful &= changesWidget.exec() && changesWidget.applyAllChanges(); // // if (!cmakeSuccessful) // { // if (KMessageBox::questionYesNo( QApplication::activeWindow(), // i18n("Changes to CMakeLists failed, abort deletion?"), // DIALOG_CAPTION ) == KMessageBox::Yes) // return false; // } // // bool ret = true; // //Then delete the files/folders // foreach(const QUrl& file, urls) // { // ret &= KDevelop::removeUrl(p, file, QDir(file.toLocalFile()).exists()); // } // // return ret; // } bool CMakeManager::removeFilesFromTargets(const QList &/*files*/) { // using namespace CMakeEdit; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Modify project targets as follows:")); // // if (!files.isEmpty() && // changesWidgetRemoveFilesFromTargets(files, &changesWidget) && // changesWidget.exec() && // changesWidget.applyAllChanges()) { // return true; // } return false; } // ProjectFolderItem* CMakeManager::addFolder(const Path& folder, ProjectFolderItem* parent) // { // using namespace CMakeEdit; // // CMakeFolderItem *cmakeParent = nearestCMakeFolder(parent); // if(!cmakeParent) // return 0; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Create folder '%1':", folder.lastPathSegment())); // // ///FIXME: use path in changes widget // changesWidgetAddFolder(folder.toUrl(), cmakeParent, &changesWidget); // // if(changesWidget.exec() && changesWidget.applyAllChanges()) // { // if(KDevelop::createFolder(folder.toUrl())) { //If saved we create the folder then the CMakeLists.txt file // Path newCMakeLists(folder, "CMakeLists.txt"); // KDevelop::createFile( newCMakeLists.toUrl() ); // } else // KMessageBox::error(0, i18n("Could not save the change."), // DIALOG_CAPTION); // } // // return 0; // } // // KDevelop::ProjectFileItem* CMakeManager::addFile( const Path& file, KDevelop::ProjectFolderItem* parent) // { // KDevelop::ProjectFileItem* created = 0; // if ( KDevelop::createFile(file.toUrl()) ) { // QList< ProjectFileItem* > files = parent->project()->filesForPath(IndexedString(file.pathOrUrl())); // if(!files.isEmpty()) // created = files.first(); // else // created = new KDevelop::ProjectFileItem( parent->project(), file, parent ); // } // return created; // } bool CMakeManager::addFilesToTarget(const QList< ProjectFileItem* > &/*_files*/, ProjectTargetItem* /*target*/) { return false; // using namespace CMakeEdit; // // const QSet headerExt = QSet() << ".h" << ".hpp" << ".hxx"; // QList< ProjectFileItem* > files = _files; // for (int i = files.count() - 1; i >= 0; --i) // { // QString fileName = files[i]->fileName(); // QString fileExt = fileName.mid(fileName.lastIndexOf('.')); // QList sameUrlItems = files[i]->project()->itemsForUrl(files[i]->url()); // if (headerExt.contains(fileExt)) // files.removeAt(i); // else foreach(ProjectBaseItem* item, sameUrlItems) // { // if (item->parent() == target) // { // files.removeAt(i); // break; // } // } // } // // if(files.isEmpty()) // return true; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Modify target '%1' as follows:", target->baseName())); // // bool success = changesWidgetAddFilesToTarget(files, target, &changesWidget) && // changesWidget.exec() && // changesWidget.applyAllChanges(); // // if(!success) // KMessageBox::error(0, i18n("CMakeLists changes failed."), DIALOG_CAPTION); // // return success; } // bool CMakeManager::renameFileOrFolder(ProjectBaseItem *item, const Path &newPath) // { // using namespace CMakeEdit; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Rename '%1' to '%2':", item->text(), // newPath.lastPathSegment())); // // bool cmakeSuccessful = true, changedCMakeLists=false; // IProject* project=item->project(); // const Path oldPath=item->path(); // QUrl oldUrl=oldPath.toUrl(); // if (item->file()) // { // QList targetFiles = cmakeListedItemsAffectedByUrlChange(project, oldUrl); // foreach(ProjectBaseItem* targetFile, targetFiles) // ///FIXME: use path in changes widget // cmakeSuccessful &= changesWidgetMoveTargetFile(targetFile, newPath.toUrl(), &changesWidget); // } // else if (CMakeFolderItem *folder = dynamic_cast(item)) // ///FIXME: use path in changes widget // cmakeSuccessful &= changesWidgetRenameFolder(folder, newPath.toUrl(), &changesWidget); // // item->setPath(newPath); // if (changesWidget.hasDocuments() && cmakeSuccessful) { // changedCMakeLists = changesWidget.exec() && changesWidget.applyAllChanges(); // cmakeSuccessful &= changedCMakeLists; // } // // if (!cmakeSuccessful) // { // if (KMessageBox::questionYesNo( QApplication::activeWindow(), // i18n("Changes to CMakeLists failed, abort rename?"), // DIALOG_CAPTION ) == KMessageBox::Yes) // return false; // } // // bool ret = KDevelop::renameUrl(project, oldUrl, newPath.toUrl()); // if(!ret) { // item->setPath(oldPath); // } // return ret; // } // // bool CMakeManager::renameFile(ProjectFileItem *item, const Path &newPath) // { // return renameFileOrFolder(item, newPath); // } // // bool CMakeManager::renameFolder(ProjectFolderItem* item, const Path &newPath) // { // return renameFileOrFolder(item, newPath); // } KTextEditor::Range CMakeManager::termRangeAtPosition(const KTextEditor::Document* textDocument, const KTextEditor::Cursor& position) const { const KTextEditor::Cursor step(0, 1); enum ParseState { NoChar, NonLeadingChar, AnyChar, }; ParseState parseState = NoChar; KTextEditor::Cursor start = position; while (true) { const QChar c = textDocument->characterAt(start); if (c.isDigit()) { parseState = NonLeadingChar; } else if (c.isLetter() || c == QLatin1Char('_')) { parseState = AnyChar; } else { // also catches going out of document range, where c is invalid break; } start -= step; } if (parseState != AnyChar) { return KTextEditor::Range::invalid(); } // undo step before last valid char start += step; KTextEditor::Cursor end = position + step; while (true) { const QChar c = textDocument->characterAt(end); if (!(c.isDigit() || c.isLetter() || c == QLatin1Char('_'))) { // also catches going out of document range, where c is invalid break; } end += step; } return KTextEditor::Range(start, end); } void CMakeManager::showConfigureErrorMessage(const QString& projectName, const QString& errorMessage) const { if (!QApplication::activeWindow()) { // Do not show a message box if there is no active window in order not to block unit tests. return; } - KMessageBox::error(QApplication::activeWindow(), i18n( + const QString messageText = i18n( "Failed to configure project '%1' (error message: %2)." " As a result, KDevelop's code understanding will likely be broken.\n" "\n" "To fix this issue, please ensure that the project's CMakeLists.txt files" " are correct, and KDevelop is configured to use the correct CMake version and settings." " Then right-click the project item in the projects tool view and click 'Reload'.", - projectName, errorMessage)); + projectName, errorMessage); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); } QPair CMakeManager::specialLanguageObjectNavigationWidget(const QUrl& url, const KTextEditor::Cursor& position) { KTextEditor::Range itemRange; CMakeNavigationWidget* doc = nullptr; KDevelop::TopDUContextPointer top= TopDUContextPointer(KDevelop::DUChain::self()->chainForDocument(url)); if(top) { int useAt=top->findUseAt(top->transformToLocalRevision(position)); if(useAt>=0) { Use u=top->uses()[useAt]; doc = new CMakeNavigationWidget(top, u.usedDeclaration(top->topContext())); itemRange = u.m_range.castToSimpleRange(); } } if (!doc) { ICMakeDocumentation* docu=CMake::cmakeDocumentation(); if( docu ) { const auto* document = ICore::self()->documentController()->documentForUrl(url); const auto* textDocument = document->textDocument(); itemRange = termRangeAtPosition(textDocument, position); if (itemRange.isValid()) { const auto id = textDocument->text(itemRange); if (!id.isEmpty()) { IDocumentation::Ptr desc=docu->description(id, url); if (desc) { doc=new CMakeNavigationWidget(top, desc); } } } } } return {doc, itemRange}; } QPair CMakeManager::cacheValue(KDevelop::IProject* /*project*/, const QString& /*id*/) const { return QPair(); } // { // QPair ret; // if(project==0 && !m_projectsData.isEmpty()) // { // project=m_projectsData.keys().first(); // } // // // qCDebug(CMAKE) << "cache value " << id << project << (m_projectsData.contains(project) && m_projectsData[project].cache.contains(id)); // CMakeProjectData* data = m_projectsData[project]; // if(data && data->cache.contains(id)) // { // const CacheEntry& e=data->cache.value(id); // ret.first=e.value; // ret.second=e.doc; // } // return ret; // }Add // void CMakeManager::projectClosing(IProject* p) { m_projects.remove(p); // delete m_projectsData.take(p); // delete m_watchers.take(p); // // m_filter->remove(p); // // qCDebug(CMAKE) << "Project closed" << p; } // // QStringList CMakeManager::processGeneratorExpression(const QStringList& expr, IProject* project, ProjectTargetItem* target) const // { // QStringList ret; // const CMakeProjectData* data = m_projectsData[project]; // GenerationExpressionSolver exec(data->properties, data->targetAlias); // if(target) // exec.setTargetName(target->text()); // // exec.defineVariable("INSTALL_PREFIX", data->vm.value("CMAKE_INSTALL_PREFIX").join(QString())); // for(QStringList::const_iterator it = expr.constBegin(), itEnd = expr.constEnd(); it!=itEnd; ++it) { // QStringList val = exec.run(*it).split(';'); // ret += val; // } // return ret; // } /* void CMakeManager::addPending(const Path& path, CMakeFolderItem* folder) { m_pending.insert(path, folder); } CMakeFolderItem* CMakeManager::takePending(const Path& path) { return m_pending.take(path); } void CMakeManager::addWatcher(IProject* p, const QString& path) { if (QFileSystemWatcher* watcher = m_watchers.value(p)) { watcher->addPath(path); } else { qCWarning(CMAKE) << "Could not find a watcher for project" << p << p->name() << ", path " << path; Q_ASSERT(false); } }*/ // CMakeProjectData CMakeManager::projectData(IProject* project) // { // Q_ASSERT(QThread::currentThread() == project->thread()); // CMakeProjectData* data = m_projectsData[project]; // if(!data) { // data = new CMakeProjectData; // m_projectsData[project] = data; // } // return *data; // } ProjectFilterManager* CMakeManager::filterManager() const { return m_filter; } void CMakeManager::dirtyFile(const QString& path) { qCDebug(CMAKE) << "dirty!" << path; //we initialize again hte project that sent the signal for(QHash::const_iterator it = m_projects.constBegin(), itEnd = m_projects.constEnd(); it!=itEnd; ++it) { if(it->watcher == sender()) { reload(it.key()->projectItem()); break; } } } void CMakeManager::folderAdded(KDevelop::ProjectFolderItem* folder) { populateTargets(folder, m_projects[folder->project()].targets); } ProjectFolderItem* CMakeManager::createFolderItem(IProject* project, const Path& path, ProjectBaseItem* parent) { // TODO: when we have data about targets, use folders with targets or similar if (QFile::exists(path.toLocalFile()+QLatin1String("/CMakeLists.txt"))) return new KDevelop::ProjectBuildFolderItem( project, path, parent ); else return KDevelop::AbstractFileManagerPlugin::createFolderItem(project, path, parent); } int CMakeManager::perProjectConfigPages() const { return 1; } ConfigPage* CMakeManager::perProjectConfigPage(int number, const ProjectConfigOptions& options, QWidget* parent) { if (number == 0) { return new CMakePreferences(this, options, parent); } return nullptr; } void CMakeManager::reloadProjects() { const auto& projects = m_projects.keys(); for (IProject* project : projects) { CMake::checkForNeedingConfigure(project); reload(project->projectItem()); } } CMakeTarget CMakeManager::targetInformation(KDevelop::ProjectTargetItem* item) const { const auto targets = m_projects[item->project()].targets[item->parent()->path()]; for (auto target: targets) { if (item->text() == target.name) { return target; } } return {}; } KDevelop::Path CMakeManager::compiler(KDevelop::ProjectTargetItem* item) const { const auto targetInfo = targetInformation(item); if (targetInfo.sources.isEmpty()) { qCDebug(CMAKE) << "could not find target" << item->text(); return {}; } const auto info = m_projects[item->project()].compilationData.files[targetInfo.sources.constFirst()]; const auto lang = info.language; if (lang.isEmpty()) { qCDebug(CMAKE) << "no language for" << item << item->text() << info.defines << targetInfo.sources.constFirst(); return {}; } const QString var = QLatin1String("CMAKE_") + lang + QLatin1String("_COMPILER"); const auto ret = CMake::readCacheValues(KDevelop::Path(buildDirectory(item), QStringLiteral("CMakeCache.txt")), {var}); qCDebug(CMAKE) << "compiler for" << lang << var << ret; return KDevelop::Path(ret.value(var)); } #include "cmakemanager.moc" diff --git a/plugins/cppcheck/job.cpp b/plugins/cppcheck/job.cpp index feae7ff977..766f6e6f6a 100644 --- a/plugins/cppcheck/job.cpp +++ b/plugins/cppcheck/job.cpp @@ -1,218 +1,221 @@ /* This file is part of KDevelop Copyright 2011 Mathieu Lornac Copyright 2011 Damien Coppel Copyright 2011 Lionel Duc Copyright 2011 Sebastien Rannou Copyright 2011 Lucas Sarie Copyright 2006-2008 Hamish Rodda Copyright 2002 Harald Fernengel Copyright 2013 Christoph Thielecke Copyright 2016-2017 Anton Anikin 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "job.h" #include "debug.h" #include "parser.h" #include "utils.h" +#include +#include +#include #include - +// KF #include -#include - +// Qt #include #include #include namespace cppcheck { Job::Job(const Parameters& params, QObject* parent) : KDevelop::OutputExecuteJob(parent) , m_timer(new QElapsedTimer) , m_parser(new CppcheckParser) , m_showXmlOutput(params.showXmlOutput) , m_projectRootPath(params.projectRootPath()) { setJobName(i18n("Cppcheck Analysis (%1)", prettyPathName(params.checkPath))); setCapabilities(KJob::Killable); setStandardToolView(KDevelop::IOutputView::TestView); setBehaviours(KDevelop::IOutputView::AutoScroll); setProperties(KDevelop::OutputExecuteJob::JobProperty::DisplayStdout); setProperties(KDevelop::OutputExecuteJob::JobProperty::DisplayStderr); setProperties(KDevelop::OutputExecuteJob::JobProperty::PostProcessOutput); *this << params.commandLine(); qCDebug(KDEV_CPPCHECK) << "checking path" << params.checkPath; } Job::~Job() { doKill(); } void Job::postProcessStdout(const QStringList& lines) { static const auto fileNameRegex = QRegularExpression(QStringLiteral("Checking ([^:]*)\\.{3}")); static const auto percentRegex = QRegularExpression(QStringLiteral("(\\d+)% done")); QRegularExpressionMatch match; for (const QString& line : lines) { match = fileNameRegex.match(line); if (match.hasMatch()) { emit infoMessage(this, match.captured(1)); continue; } match = percentRegex.match(line); if (match.hasMatch()) { setPercent(match.capturedRef(1).toULong()); continue; } } m_standardOutput << lines; if (status() == KDevelop::OutputExecuteJob::JobStatus::JobRunning) { KDevelop::OutputExecuteJob::postProcessStdout(lines); } } void Job::postProcessStderr(const QStringList& lines) { static const auto xmlStartRegex = QRegularExpression(QStringLiteral("\\s*<")); for (const QString & line : lines) { // unfortunately sometime cppcheck send non-XML messages to stderr. // For example, if we pass '-I /missing_include_dir' to the argument list, // then stderr output will contains such line (tested on cppcheck 1.72): // // (information) Couldn't find path given by -I '/missing_include_dir' // // Therefore we must 'move' such messages to m_standardOutput. if (line.indexOf(xmlStartRegex) != -1) { // the line contains XML m_xmlOutput << line; m_parser->addData(line); m_problems = m_parser->parse(); emitProblems(); } else { KDevelop::IProblem::Ptr problem(new KDevelop::DetectedProblem(i18n("Cppcheck"))); problem->setSeverity(KDevelop::IProblem::Error); problem->setDescription(line); problem->setExplanation(QStringLiteral("Check your cppcheck settings")); m_problems = {problem}; emitProblems(); if (m_showXmlOutput) { m_standardOutput << line; } else { postProcessStdout({line}); } } } if (status() == KDevelop::OutputExecuteJob::JobStatus::JobRunning && m_showXmlOutput) { KDevelop::OutputExecuteJob::postProcessStderr(lines); } } void Job::start() { m_standardOutput.clear(); m_xmlOutput.clear(); qCDebug(KDEV_CPPCHECK) << "executing:" << commandLine().join(QLatin1Char(' ')); m_timer->restart(); KDevelop::OutputExecuteJob::start(); } void Job::childProcessError(QProcess::ProcessError e) { - QString message; + QString messageText; switch (e) { case QProcess::FailedToStart: - message = i18n("Failed to start Cppcheck from \"%1\".", commandLine()[0]); + messageText = i18n("Failed to start Cppcheck from \"%1\".", commandLine()[0]); break; case QProcess::Crashed: if (status() != KDevelop::OutputExecuteJob::JobStatus::JobCanceled) { - message = i18n("Cppcheck crashed."); + messageText = i18n("Cppcheck crashed."); } break; case QProcess::Timedout: - message = i18n("Cppcheck process timed out."); + messageText = i18n("Cppcheck process timed out."); break; case QProcess::WriteError: - message = i18n("Write to Cppcheck process failed."); + messageText = i18n("Write to Cppcheck process failed."); break; case QProcess::ReadError: - message = i18n("Read from Cppcheck process failed."); + messageText = i18n("Read from Cppcheck process failed."); break; case QProcess::UnknownError: // current cppcheck errors will be displayed in the output view // don't notify the user break; } - if (!message.isEmpty()) { - KMessageBox::error(qApp->activeWindow(), message, i18n("Cppcheck Error")); + if (!messageText.isEmpty()) { + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); } KDevelop::OutputExecuteJob::childProcessError(e); } void Job::childProcessExited(int exitCode, QProcess::ExitStatus exitStatus) { qCDebug(KDEV_CPPCHECK) << "Process Finished, exitCode" << exitCode << "process exit status" << exitStatus; postProcessStdout({QStringLiteral("Elapsed time: %1 s.").arg(m_timer->elapsed()/1000.0)}); if (exitCode != 0) { qCDebug(KDEV_CPPCHECK) << "cppcheck failed, standard output: "; qCDebug(KDEV_CPPCHECK) << m_standardOutput.join(QLatin1Char('\n')); qCDebug(KDEV_CPPCHECK) << "cppcheck failed, XML output: "; qCDebug(KDEV_CPPCHECK) << m_xmlOutput.join(QLatin1Char('\n')); } KDevelop::OutputExecuteJob::childProcessExited(exitCode, exitStatus); } void Job::emitProblems() { if (!m_problems.isEmpty()) { emit problemsDetected(m_problems); } } } diff --git a/plugins/cppcheck/parser.cpp b/plugins/cppcheck/parser.cpp index 582298020f..5ffd756673 100644 --- a/plugins/cppcheck/parser.cpp +++ b/plugins/cppcheck/parser.cpp @@ -1,307 +1,309 @@ /* This file is part of KDevelop Copyright 2013 Christoph Thielecke Copyright 2016 Anton Anikin 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "parser.h" #include "debug.h" +#include +#include #include +#include #include - +// KF #include -#include - +// Qt #include #include namespace cppcheck { /** * Convert the value of \ attribute of \ element from cppcheck's * XML-output to 'good-looking' HTML-version. This is necessary because the * displaying of the original message is performed without line breaks - such * tooltips are uncomfortable to read, and large messages will not fit into the * screen. * * This function put the original message into \ tag that automatically * provides line wrapping by builtin capabilities of Qt library. The source text * also can contain tokens '\012' (line break) - they are present in the case of * source code examples. In such cases, the entire text between the first and * last tokens (i.e. source code) is placed into \ tag. * * @param[in] input the original value of \ attribute * @return HTML version for displaying in problem's tooltip */ QString verboseMessageToHtml( const QString & input ) { QString output(QStringLiteral("%1").arg(input.toHtmlEscaped())); output.replace(QLatin1String("\\012"), QLatin1String("\n")); if (output.count(QLatin1Char('\n')) >= 2) { output.replace(output.indexOf(QLatin1Char('\n')), 1, QStringLiteral("
") );
         output.replace(output.lastIndexOf(QLatin1Char('\n')), 1, QStringLiteral("

") ); } return output; } CppcheckParser::CppcheckParser() { } CppcheckParser::~CppcheckParser() { } void CppcheckParser::clear() { m_stateStack.clear(); } bool CppcheckParser::startElement() { State newState = Unknown; qCDebug(KDEV_CPPCHECK) << "CppcheckParser::startElement: elem: " << qPrintable(name().toString()); if (name() == QLatin1String("results")) { newState = Results; } else if (name() == QLatin1String("cppcheck")) { newState = CppCheck; } else if (name() == QLatin1String("errors")) { newState = Errors; } else if (name() == QLatin1String("location")) { newState = Location; if (attributes().hasAttribute(QStringLiteral("file")) && attributes().hasAttribute(QStringLiteral("line"))) { QString errorFile = attributes().value(QStringLiteral("file")).toString(); // Usually when "file0" attribute exists it associated with source and // attribute "file" associated with header). // But sometimes cppcheck produces errors with "file" and "file0" attributes // both associated with same *source* file. In such cases attribute "file" contains // only file name, without full path. Therefore we should use "file0" instead "file". if (!QFile::exists(errorFile) && attributes().hasAttribute(QStringLiteral("file0"))) { errorFile = attributes().value(QStringLiteral("file0")).toString(); } m_errorFiles += errorFile; m_errorLines += attributes().value(QStringLiteral("line")).toString().toInt(); } } else if (name() == QLatin1String("error")) { newState = Error; m_errorSeverity = QStringLiteral("unknown"); m_errorInconclusive = false; m_errorFiles.clear(); m_errorLines.clear(); m_errorMessage.clear(); m_errorVerboseMessage.clear(); if (attributes().hasAttribute(QStringLiteral("msg"))) { m_errorMessage = attributes().value(QStringLiteral("msg")).toString(); } if (attributes().hasAttribute(QStringLiteral("verbose"))) { m_errorVerboseMessage = verboseMessageToHtml(attributes().value(QStringLiteral("verbose")).toString()); } if (attributes().hasAttribute(QStringLiteral("severity"))) { m_errorSeverity = attributes().value(QStringLiteral("severity")).toString(); } if (attributes().hasAttribute(QStringLiteral("inconclusive"))) { m_errorInconclusive = true; } } else { m_stateStack.push(m_stateStack.top()); return true; } m_stateStack.push(newState); return true; } bool CppcheckParser::endElement(QVector& problems) { qCDebug(KDEV_CPPCHECK) << "CppcheckParser::endElement: elem: " << qPrintable(name().toString()); State state = m_stateStack.pop(); switch (state) { case CppCheck: if (attributes().hasAttribute(QStringLiteral("version"))) { qCDebug(KDEV_CPPCHECK) << "Cppcheck report version: " << attributes().value(QStringLiteral("version")); } break; case Errors: // errors finished break; case Error: qCDebug(KDEV_CPPCHECK) << "CppcheckParser::endElement: new error elem: line: " << (m_errorLines.isEmpty() ? QStringLiteral("?") : QString::number(m_errorLines.first())) << " at " << (m_errorFiles.isEmpty() ? QStringLiteral("?") : m_errorFiles.first()) << ", msg: " << m_errorMessage; storeError(problems); break; case Results: // results finished break; case Location: break; default: break; } return true; } QVector CppcheckParser::parse() { QVector problems; qCDebug(KDEV_CPPCHECK) << "CppcheckParser::parse!"; while (!atEnd()) { int readNextVal = readNext(); switch (readNextVal) { case StartDocument: clear(); break; case StartElement: startElement(); break; case EndElement: endElement(problems); break; case Characters: break; default: qCDebug(KDEV_CPPCHECK) << "CppcheckParser::startElement: case: " << readNextVal; break; } } qCDebug(KDEV_CPPCHECK) << "CppcheckParser::parse: end"; if (hasError()) { switch (error()) { case CustomError: case UnexpectedElementError: - case NotWellFormedError: - KMessageBox::error( - qApp->activeWindow(), - i18n("Cppcheck XML Parsing: error at line %1, column %2: %3", lineNumber(), columnNumber(), errorString()), - i18n("Cppcheck Error")); + case NotWellFormedError: { + const QString messageText = + i18n("Cppcheck XML Parsing: error at line %1, column %2: %3", lineNumber(), columnNumber(), errorString()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); break; - + } case NoError: case PrematureEndOfDocumentError: break; } } return problems; } void CppcheckParser::storeError(QVector& problems) { // Construct problem with using first location element KDevelop::IProblem::Ptr problem = getProblem(); // Adds other elements as diagnostics. // This allows the user to track the problem. for (int locationIdx = 1; locationIdx < m_errorFiles.size(); ++locationIdx) { problem->addDiagnostic(getProblem(locationIdx)); } problems.push_back(problem); } KDevelop::IProblem::Ptr CppcheckParser::getProblem(int locationIdx) const { KDevelop::IProblem::Ptr problem(new KDevelop::DetectedProblem(i18n("Cppcheck"))); QStringList messagePrefix; QString errorMessage(m_errorMessage); if (m_errorSeverity == QLatin1String("error")) { problem->setSeverity(KDevelop::IProblem::Error); } else if (m_errorSeverity == QLatin1String("warning")) { problem->setSeverity(KDevelop::IProblem::Warning); } else { problem->setSeverity(KDevelop::IProblem::Hint); messagePrefix.push_back(m_errorSeverity); } if (m_errorInconclusive) { messagePrefix.push_back(QStringLiteral("inconclusive")); } if (!messagePrefix.isEmpty()) { errorMessage = QStringLiteral("(%1) %2").arg(messagePrefix.join(QLatin1String(", ")), m_errorMessage); } problem->setDescription(errorMessage); problem->setExplanation(m_errorVerboseMessage); KDevelop::DocumentRange range; if (locationIdx < 0 || locationIdx >= m_errorFiles.size()) { range = KDevelop::DocumentRange::invalid(); } else { range.document = KDevelop::IndexedString(m_errorFiles.at(locationIdx)); range.setBothLines(m_errorLines.at(locationIdx) - 1); range.setBothColumns(0); } problem->setFinalLocation(range); problem->setFinalLocationMode(KDevelop::IProblem::TrimmedLine); return problem; } } diff --git a/plugins/debuggercommon/midebugger.cpp b/plugins/debuggercommon/midebugger.cpp index 1009fb8527..c5cb167afe 100644 --- a/plugins/debuggercommon/midebugger.cpp +++ b/plugins/debuggercommon/midebugger.cpp @@ -1,370 +1,374 @@ /* * Low level GDB interface. * * Copyright 1999 John Birch * Copyright 2007 Vladimir Prus * Copyright 2016 Aetf * * 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, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "midebugger.h" #include "debuglog.h" #include "mi/micommand.h" +#include +#include +#include + #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #endif // #define DEBUG_NO_TRY //to get a backtrace to where the exception was thrown using namespace KDevMI; using namespace KDevMI::MI; MIDebugger::MIDebugger(QObject* parent) : QObject(parent) { m_process = new KProcess(this); m_process->setOutputChannelMode(KProcess::SeparateChannels); connect(m_process, &KProcess::readyReadStandardOutput, this, &MIDebugger::readyReadStandardOutput); connect(m_process, &KProcess::readyReadStandardError, this, &MIDebugger::readyReadStandardError); connect(m_process, QOverload::of(&QProcess::finished), this, &MIDebugger::processFinished); connect(m_process, &QProcess::errorOccurred, this, &MIDebugger::processErrored); } MIDebugger::~MIDebugger() { // prevent Qt warning: QProcess: Destroyed while process is still running. if (m_process && m_process->state() == QProcess::Running) { disconnect(m_process, &QProcess::errorOccurred, this, &MIDebugger::processErrored); m_process->kill(); m_process->waitForFinished(10); } } void MIDebugger::execute(MICommand* command) { m_currentCmd = command; QString commandText = m_currentCmd->cmdToSend(); qCDebug(DEBUGGERCOMMON) << "SEND:" << commandText.trimmed(); QByteArray commandUtf8 = commandText.toUtf8(); m_process->write(commandUtf8); command->markAsSubmitted(); QString prettyCmd = m_currentCmd->cmdToSend(); prettyCmd.remove(QRegExp(QStringLiteral("set prompt \032.\n"))); prettyCmd = QLatin1String("(gdb) ") + prettyCmd; if (m_currentCmd->isUserCommand()) emit userCommandOutput(prettyCmd); else emit internalCommandOutput(prettyCmd); } bool MIDebugger::isReady() const { return m_currentCmd == nullptr; } void MIDebugger::interrupt() { #ifndef Q_OS_WIN int pid = m_process->pid(); if (pid != 0) { ::kill(pid, SIGINT); } #else SetConsoleCtrlHandler(nullptr, true); GenerateConsoleCtrlEvent(0, 0); SetConsoleCtrlHandler(nullptr, false); #endif } MICommand* MIDebugger::currentCommand() const { return m_currentCmd; } void MIDebugger::kill() { m_process->kill(); } void MIDebugger::readyReadStandardOutput() { m_process->setReadChannel(QProcess::StandardOutput); m_buffer += m_process->readAll(); for (;;) { /* In MI mode, all messages are exactly one line. See if we have any complete lines in the buffer. */ int i = m_buffer.indexOf('\n'); if (i == -1) break; QByteArray reply(m_buffer.left(i)); m_buffer.remove(0, i+1); processLine(reply); } } void MIDebugger::readyReadStandardError() { m_process->setReadChannel(QProcess::StandardError); emit debuggerInternalOutput(QString::fromUtf8(m_process->readAll())); } void MIDebugger::processLine(const QByteArray& line) { if (line != "(gdb) ") { qCDebug(DEBUGGERCOMMON) << "Debugger output (pid =" << m_process->pid() << "): " << line; } FileSymbol file; file.contents = line; std::unique_ptr r(m_parser.parse(&file)); if (!r) { // simply ignore the invalid MI message because both gdb and lldb // sometimes produces invalid messages that can be safely ignored. qCDebug(DEBUGGERCOMMON) << "Invalid MI message:" << line; // We don't consider the current command done. // So, if a command results in unparseable reply, // we'll just wait for the "right" reply, which might // never come. However, marking the command as // done in this case is even more risky. // It's probably possible to get here if we're debugging // natively without PTY, though this is uncommon case. return; } #ifndef DEBUG_NO_TRY try { #endif switch(r->kind) { case MI::Record::Result: { auto& result = static_cast(*r); // it's still possible for the user to issue a MI command, // emit correct signal if (m_currentCmd && m_currentCmd->isUserCommand()) { emit userCommandOutput(QString::fromUtf8(line) + QLatin1Char('\n')); } else { emit internalCommandOutput(QString::fromUtf8(line) + QLatin1Char('\n')); } // protect against wild replies that sometimes returned from gdb without a pending command if (!m_currentCmd) { qCWarning(DEBUGGERCOMMON) << "Received a result without a pending command"; throw std::runtime_error("Received a result without a pending command"); } else if (m_currentCmd->token() != result.token) { std::stringstream ss; ss << "Received a result with token not matching pending command. " << "Pending: " << m_currentCmd->token() << "Received: " << result.token; qCWarning(DEBUGGERCOMMON) << ss.str().c_str(); throw std::runtime_error(ss.str()); } // GDB doc: "running" and "exit" are status codes equivalent to "done" if (result.reason == QLatin1String("done") || result.reason == QLatin1String("running") || result.reason == QLatin1String("exit")) { qCDebug(DEBUGGERCOMMON) << "Result token is" << result.token; m_currentCmd->markAsCompleted(); qCDebug(DEBUGGERCOMMON) << "Command successful, times " << m_currentCmd->totalProcessingTime() << m_currentCmd->queueTime() << m_currentCmd->gdbProcessingTime(); m_currentCmd->invokeHandler(result); } else if (result.reason == QLatin1String("error")) { qCDebug(DEBUGGERCOMMON) << "Handling error"; m_currentCmd->markAsCompleted(); qCDebug(DEBUGGERCOMMON) << "Command error, times" << m_currentCmd->totalProcessingTime() << m_currentCmd->queueTime() << m_currentCmd->gdbProcessingTime(); // Some commands want to handle errors themself. if (m_currentCmd->handlesError() && m_currentCmd->invokeHandler(result)) { qCDebug(DEBUGGERCOMMON) << "Invoked custom handler\n"; // Done, nothing more needed } else emit error(result); } else { qCDebug(DEBUGGERCOMMON) << "Unhandled result code: " << result.reason; } delete m_currentCmd; m_currentCmd = nullptr; emit ready(); break; } case MI::Record::Async: { auto& async = static_cast(*r); switch (async.subkind) { case MI::AsyncRecord::Exec: { // Prefix '*'; asynchronous state changes of the target if (async.reason == QLatin1String("stopped")) { emit programStopped(async); } else if (async.reason == QLatin1String("running")) { emit programRunning(); } else { qCDebug(DEBUGGERCOMMON) << "Unhandled exec notification: " << async.reason; } break; } case MI::AsyncRecord::Notify: { // Prefix '='; supplementary information that we should handle (new breakpoint etc.) emit notification(async); break; } case MI::AsyncRecord::Status: { // Prefix '+'; GDB documentation: // On-going status information about progress of a slow operation; may be ignored break; } } break; } case MI::Record::Stream: { auto& s = static_cast(*r); if (s.subkind == MI::StreamRecord::Target) { emit applicationOutput(s.message); } else if (s.subkind == MI::StreamRecord::Console) { if (m_currentCmd && m_currentCmd->isUserCommand()) emit userCommandOutput(s.message); else emit internalCommandOutput(s.message); if (m_currentCmd) m_currentCmd->newOutput(s.message); } else { emit debuggerInternalOutput(s.message); } emit streamRecord(s); break; } case MI::Record::Prompt: break; } #ifndef DEBUG_NO_TRY } catch(const std::exception& e) { KMessageBox::detailedSorry( qApp->activeWindow(), i18nc("Internal debugger error", "

The debugger component encountered an internal error while " "processing the reply from the debugger. Please submit a bug report. " "The debug session will now end to prevent potential crash"), i18n("The exception is: %1\n" "The MI response is: %2", QString::fromUtf8(e.what()), QString::fromLatin1(line)), i18n("Internal debugger error")); emit exited(true, QString::fromUtf8(e.what())); } #endif } void MIDebugger::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { qCDebug(DEBUGGERCOMMON) << "Debugger FINISHED\n"; bool abnormal = exitCode != 0 || exitStatus != QProcess::NormalExit; emit userCommandOutput(QStringLiteral("Process exited\n")); emit exited(abnormal, i18n("Process exited")); } void MIDebugger::processErrored(QProcess::ProcessError error) { qCWarning(DEBUGGERCOMMON) << "Debugger ERRORED" << error << m_process->errorString(); if(error == QProcess::FailedToStart) { - KMessageBox::information( - qApp->activeWindow(), + const QString messageText = i18n("Could not start debugger." "

Could not run '%1'. " "Make sure that the path name is specified correctly.", - m_debuggerExecutable), - i18n("Could not start debugger")); + m_debuggerExecutable); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); emit userCommandOutput(QStringLiteral("Process failed to start\n")); emit exited(true, i18n("Process failed to start")); } else if (error == QProcess::Crashed) { KMessageBox::error( qApp->activeWindow(), i18n("Debugger crashed." "

The debugger process '%1' crashed.
" "Because of that the debug session has to be ended.
" "Try to reproduce the crash without KDevelop and report a bug.
", m_debuggerExecutable), i18n("Debugger crashed")); emit userCommandOutput(QStringLiteral("Process crashed\n")); emit exited(true, i18n("Process crashed")); } } diff --git a/plugins/debuggercommon/midebuggerplugin.cpp b/plugins/debuggercommon/midebuggerplugin.cpp index 50309e99e4..cdd1002496 100644 --- a/plugins/debuggercommon/midebuggerplugin.cpp +++ b/plugins/debuggercommon/midebuggerplugin.cpp @@ -1,312 +1,316 @@ /* * Common code for MI debugger support * * Copyright 1999-2001 John Birch * Copyright 2001 by Bernd Gehrmann * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * Copyright 2016 Aetf * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "midebuggerplugin.h" #include "midebugjobs.h" #include "dialogs/processselection.h" #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; using namespace KDevMI; class KDevMI::DBusProxy : public QObject { Q_OBJECT public: DBusProxy(const QString& service, const QString& name, QObject* parent) : QObject(parent), m_dbusInterface(service, QStringLiteral("/debugger")), m_name(name), m_valid(true) {} ~DBusProxy() override { if (m_valid) { m_dbusInterface.call(QStringLiteral("debuggerClosed"), m_name); } } QDBusInterface* interface() { return &m_dbusInterface; } void Invalidate() { m_valid = false; } public Q_SLOTS: void debuggerAccepted(const QString& name) { if (name == m_name) { emit debugProcess(this); } } void debuggingFinished() { m_dbusInterface.call(QStringLiteral("debuggingFinished"), m_name); } Q_SIGNALS: void debugProcess(DBusProxy*); private: QDBusInterface m_dbusInterface; QString m_name; bool m_valid; }; MIDebuggerPlugin::MIDebuggerPlugin(const QString &componentName, const QString& displayName, QObject *parent) : KDevelop::IPlugin(componentName, parent), m_displayName(displayName) { core()->debugController()->initializeUi(); setupActions(); setupDBus(); } void MIDebuggerPlugin::setupActions() { KActionCollection* ac = actionCollection(); auto * action = new QAction(this); action->setIcon(QIcon::fromTheme(QStringLiteral("core"))); action->setText(i18n("Examine Core File with %1", m_displayName)); action->setWhatsThis(i18n("Examine core file" "

This loads a core file, which is typically created " "after the application has crashed, e.g. with a " "segmentation fault. The core file contains an " "image of the program memory at the time it crashed, " "allowing you to do a post-mortem analysis.

")); connect(action, &QAction::triggered, this, &MIDebuggerPlugin::slotExamineCore); ac->addAction(QStringLiteral("debug_core"), action); #if KF5SysGuard_FOUND action = new QAction(this); action->setIcon(QIcon::fromTheme(QStringLiteral("connect_creating"))); action->setText(i18n("Attach to Process with %1", m_displayName)); action->setWhatsThis(i18n("Attach to process" "

Attaches the debugger to a running process.

")); connect(action, &QAction::triggered, this, &MIDebuggerPlugin::slotAttachProcess); ac->addAction(QStringLiteral("debug_attach"), action); #endif } void MIDebuggerPlugin::setupDBus() { QDBusConnectionInterface* dbusInterface = QDBusConnection::sessionBus().interface(); const auto& registeredServiceNames = dbusInterface->registeredServiceNames().value(); for (const auto& service : registeredServiceNames) { slotDBusOwnerChanged(service, QString(), QStringLiteral("n")); } connect(dbusInterface, &QDBusConnectionInterface::serviceOwnerChanged, this, &MIDebuggerPlugin::slotDBusOwnerChanged); } void MIDebuggerPlugin::unload() { unloadToolViews(); } MIDebuggerPlugin::~MIDebuggerPlugin() { } void MIDebuggerPlugin::slotDBusOwnerChanged(const QString& service, const QString& oldOwner, const QString& newOwner) { if (oldOwner.isEmpty() && service.startsWith(QLatin1String("org.kde.drkonqi"))) { if (m_drkonqis.contains(service)) { return; } // New registration const QString name = i18n("KDevelop (%1) - %2", m_displayName, core()->activeSession()->name()); auto drkonqiProxy = new DBusProxy(service, name, this); m_drkonqis.insert(service, drkonqiProxy); connect(drkonqiProxy->interface(), SIGNAL(acceptDebuggingApplication(QString)), drkonqiProxy, SLOT(debuggerAccepted(QString))); connect(drkonqiProxy, &DBusProxy::debugProcess, this, &MIDebuggerPlugin::slotDebugExternalProcess); drkonqiProxy->interface()->call(QStringLiteral("registerDebuggingApplication"), name, QCoreApplication::applicationPid()); } else if (newOwner.isEmpty() && service.startsWith(QLatin1String("org.kde.drkonqi"))) { // Deregistration const auto proxyIt = m_drkonqis.find(service); if (proxyIt != m_drkonqis.end()) { auto proxy = *proxyIt; m_drkonqis.erase(proxyIt); proxy->Invalidate(); delete proxy; } } } void MIDebuggerPlugin::slotDebugExternalProcess(DBusProxy* proxy) { QDBusReply reply = proxy->interface()->call(QStringLiteral("pid")); if (reply.isValid()) { connect(attachProcess(reply.value()), &KJob::result, proxy, &DBusProxy::debuggingFinished); } core()->uiController()->activeMainWindow()->raise(); } ContextMenuExtension MIDebuggerPlugin::contextMenuExtension(Context* context, QWidget* parent) { ContextMenuExtension menuExt = IPlugin::contextMenuExtension(context, parent); if (context->type() != KDevelop::Context::EditorContext) return menuExt; auto *econtext = dynamic_cast(context); if (!econtext) return menuExt; QString contextIdent = econtext->currentWord(); if (!contextIdent.isEmpty()) { QString squeezed = KStringHandler::csqueeze(contextIdent, 30); auto* action = new QAction(parent); action->setText(i18n("Evaluate: %1", squeezed)); action->setWhatsThis(i18n("Evaluate expression" "

Shows the value of the expression under the cursor.

")); connect(action, &QAction::triggered, this, [this, contextIdent](){ emit addWatchVariable(contextIdent); }); menuExt.addAction(ContextMenuExtension::DebugGroup, action); action = new QAction(parent); action->setText(i18n("Watch: %1", squeezed)); action->setWhatsThis(i18n("Watch expression" "

Adds the expression under the cursor to the Variables/Watch list.

")); connect(action, &QAction::triggered, this, [this, contextIdent](){ emit evaluateExpression(contextIdent); }); menuExt.addAction(ContextMenuExtension::DebugGroup, action); } return menuExt; } void MIDebuggerPlugin::slotExamineCore() { showStatusMessage(i18n("Choose a core file to examine..."), 1000); if (core()->debugController()->currentSession() != nullptr) { KMessageBox::ButtonCode answer = KMessageBox::warningYesNo( core()->uiController()->activeMainWindow(), i18n("A program is already being debugged. Do you want to abort the " "currently running debug session and continue?")); if (answer == KMessageBox::No) return; } auto *job = new MIExamineCoreJob(this, core()->runController()); core()->runController()->registerJob(job); // job->start() is called in registerJob } #if KF5SysGuard_FOUND void MIDebuggerPlugin::slotAttachProcess() { showStatusMessage(i18n("Choose a process to attach to..."), 1000); if (core()->debugController()->currentSession() != nullptr) { KMessageBox::ButtonCode answer = KMessageBox::warningYesNo( core()->uiController()->activeMainWindow(), i18n("A program is already being debugged. Do you want to abort the " "currently running debug session and continue?")); if (answer == KMessageBox::No) return; } QPointer dlg = new ProcessSelectionDialog(core()->uiController()->activeMainWindow()); if (!dlg->exec() || !dlg->pidSelected()) { delete dlg; return; } // TODO: move check into process selection dialog int pid = dlg->pidSelected(); delete dlg; - if (QApplication::applicationPid() == pid) - KMessageBox::error(core()->uiController()->activeMainWindow(), - i18n("Not attaching to process %1: cannot attach the debugger to itself.", pid)); + if (QApplication::applicationPid() == pid) { + const QString messageText = + i18n("Not attaching to process %1: cannot attach the debugger to itself.", pid); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); + } else attachProcess(pid); } #endif MIAttachProcessJob* MIDebuggerPlugin::attachProcess(int pid) { auto *job = new MIAttachProcessJob(this, pid, core()->runController()); core()->runController()->registerJob(job); // job->start() is called in registerJob return job; } QString MIDebuggerPlugin::statusName() const { return i18n("Debugger"); } void MIDebuggerPlugin::showStatusMessage(const QString& msg, int timeout) { emit showMessage(this, msg, timeout); } #include "midebuggerplugin.moc" diff --git a/plugins/debuggercommon/midebugsession.cpp b/plugins/debuggercommon/midebugsession.cpp index 7a833d2389..4a55e819d2 100644 --- a/plugins/debuggercommon/midebugsession.cpp +++ b/plugins/debuggercommon/midebugsession.cpp @@ -1,1302 +1,1304 @@ /* * Common code for debugger support * * Copyright 1999-2001 John Birch * Copyright 2001 by Bernd Gehrmann * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * Copyright 2009 Niko Sams * Copyright 2016 Aetf * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "midebugsession.h" #include "debuglog.h" #include "midebugger.h" #include "mivariable.h" #include "mi/mi.h" #include "mi/micommand.h" #include "mi/micommandqueue.h" #include "stty.h" #include #include #include #include #include #include +#include +#include #include #include #include -#include #include #include #include #include #include #include #include #include using namespace KDevelop; using namespace KDevMI; using namespace KDevMI::MI; MIDebugSession::MIDebugSession(MIDebuggerPlugin *plugin) : m_procLineMaker(new ProcessLineMaker(this)) , m_commandQueue(new CommandQueue) , m_debuggerState(s_dbgNotStarted | s_appNotStarted) , m_tty(nullptr) , m_plugin(plugin) { // setup signals connect(m_procLineMaker, &ProcessLineMaker::receivedStdoutLines, this, &MIDebugSession::inferiorStdoutLines); connect(m_procLineMaker, &ProcessLineMaker::receivedStderrLines, this, &MIDebugSession::inferiorStderrLines); // forward tty output to process line maker connect(this, &MIDebugSession::inferiorTtyStdout, m_procLineMaker, &ProcessLineMaker::slotReceivedStdout); connect(this, &MIDebugSession::inferiorTtyStderr, m_procLineMaker, &ProcessLineMaker::slotReceivedStderr); // FIXME: see if this still works //connect(statusBarIndicator, SIGNAL(doubleClicked()), // controller, SLOT(explainDebuggerStatus())); // FIXME: reimplement / re-enable //connect(this, SIGNAL(addWatchVariable(QString)), controller->variables(), SLOT(slotAddWatchVariable(QString))); //connect(this, SIGNAL(evaluateExpression(QString)), controller->variables(), SLOT(slotEvaluateExpression(QString))); } MIDebugSession::~MIDebugSession() { qCDebug(DEBUGGERCOMMON) << "Destroying MIDebugSession"; // Deleting the session involves shutting down gdb nicely. // When were attached to a process, we must first detach so that the process // can continue running as it was before being attached. gdb is quite slow to // detach from a process, so we must process events within here to get a "clean" // shutdown. if (!debuggerStateIsOn(s_dbgNotStarted)) { stopDebugger(); } } IDebugSession::DebuggerState MIDebugSession::state() const { return m_sessionState; } QMap & MIDebugSession::variableMapping() { return m_allVariables; } MIVariable* MIDebugSession::findVariableByVarobjName(const QString &varobjName) const { if (m_allVariables.count(varobjName) == 0) return nullptr; return m_allVariables.value(varobjName); } void MIDebugSession::markAllVariableDead() { for (auto* variable : qAsConst(m_allVariables)) { variable->markAsDead(); } m_allVariables.clear(); } bool MIDebugSession::restartAvaliable() const { if (debuggerStateIsOn(s_attached) || debuggerStateIsOn(s_core)) { return false; } else { return true; } } bool MIDebugSession::startDebugger(ILaunchConfiguration *cfg) { qCDebug(DEBUGGERCOMMON) << "Starting new debugger instance"; if (m_debugger) { qCWarning(DEBUGGERCOMMON) << "m_debugger object still exists"; delete m_debugger; m_debugger = nullptr; } m_debugger = createDebugger(); m_debugger->setParent(this); // output signals connect(m_debugger, &MIDebugger::applicationOutput, this, [this](const QString &output) { auto lines = output.split(QRegularExpression(QStringLiteral("[\r\n]")), QString::SkipEmptyParts); for (auto &line : lines) { int p = line.length(); while (p >= 1 && (line[p-1] == QLatin1Char('\r') || line[p-1] == QLatin1Char('\n'))) { p--; } if (p != line.length()) line.truncate(p); } emit inferiorStdoutLines(lines); }); connect(m_debugger, &MIDebugger::userCommandOutput, this, &MIDebugSession::debuggerUserCommandOutput); connect(m_debugger, &MIDebugger::internalCommandOutput, this, &MIDebugSession::debuggerInternalCommandOutput); connect(m_debugger, &MIDebugger::debuggerInternalOutput, this, &MIDebugSession::debuggerInternalOutput); // state signals connect(m_debugger, &MIDebugger::programStopped, this, &MIDebugSession::inferiorStopped); connect(m_debugger, &MIDebugger::programRunning, this, &MIDebugSession::inferiorRunning); // internal handlers connect(m_debugger, &MIDebugger::ready, this, &MIDebugSession::slotDebuggerReady); connect(m_debugger, &MIDebugger::exited, this, &MIDebugSession::slotDebuggerExited); connect(m_debugger, &MIDebugger::programStopped, this, &MIDebugSession::slotInferiorStopped); connect(m_debugger, &MIDebugger::programRunning, this, &MIDebugSession::slotInferiorRunning); connect(m_debugger, &MIDebugger::notification, this, &MIDebugSession::processNotification); // start the debugger. Do this after connecting all signals so that initial // debugger output, and important events like the debugger died are reported. QStringList extraArguments; if (!m_sourceInitFile) extraArguments << QStringLiteral("--nx"); auto config = cfg ? cfg->config() // FIXME: this is only used when attachToProcess or examineCoreFile. // Change to use a global launch configuration when calling : KConfigGroup(KSharedConfig::openConfig(), "GDB Config"); if (!m_debugger->start(config, extraArguments)) { // debugger failed to start, ensure debugger and session state are correctly updated. setDebuggerStateOn(s_dbgFailedStart); return false; } // FIXME: here, we should wait until the debugger is up and waiting for input. // Then, clear s_dbgNotStarted // It's better to do this right away so that the state bit is always correct. setDebuggerStateOff(s_dbgNotStarted); // Initialise debugger. At this stage debugger is sitting wondering what to do, // and to whom. initializeDebugger(); qCDebug(DEBUGGERCOMMON) << "Debugger instance started"; return true; } bool MIDebugSession::startDebugging(ILaunchConfiguration* cfg, IExecutePlugin* iexec) { qCDebug(DEBUGGERCOMMON) << "Starting new debug session"; Q_ASSERT(cfg); Q_ASSERT(iexec); // Ensure debugger is started first if (debuggerStateIsOn(s_appNotStarted)) { emit showMessage(i18n("Running program"), 1000); } if (debuggerStateIsOn(s_dbgNotStarted)) { if (!startDebugger(cfg)) return false; } if (debuggerStateIsOn(s_shuttingDown)) { qCDebug(DEBUGGERCOMMON) << "Tried to run when debugger shutting down"; return false; } // Only dummy err here, actual erros have been checked already in the job and we don't get here if there were any QString err; QString executable = iexec->executable(cfg, err).toLocalFile(); configInferior(cfg, iexec, executable); // Set up the tty for the inferior bool config_useExternalTerminal = iexec->useTerminal(cfg); QString config_ternimalName = iexec->terminal(cfg); if (!config_ternimalName.isEmpty()) { // the external terminal cmd contains additional arguments, just get the terminal name config_ternimalName = KShell::splitArgs(config_ternimalName).first(); } m_tty.reset(new STTY(config_useExternalTerminal, config_ternimalName)); if (!config_useExternalTerminal) { connect(m_tty.get(), &STTY::OutOutput, this, &MIDebugSession::inferiorTtyStdout); connect(m_tty.get(), &STTY::ErrOutput, this, &MIDebugSession::inferiorTtyStderr); } QString tty(m_tty->getSlave()); #ifndef Q_OS_WIN if (tty.isEmpty()) { - KMessageBox::information(qApp->activeWindow(), m_tty->lastError(), i18n("warning")); + auto* message = new Sublime::Message(m_tty->lastError(), Sublime::Message::Information); + ICore::self()->uiController()->postMessage(message); m_tty.reset(nullptr); return false; } #endif addCommand(InferiorTtySet, tty); // Change the working directory to the correct one QString dir = iexec->workingDirectory(cfg).toLocalFile(); if (dir.isEmpty()) { dir = QFileInfo(executable).absolutePath(); } addCommand(EnvironmentCd, QLatin1Char('"') + dir + QLatin1Char('"')); // Set the run arguments QStringList arguments = iexec->arguments(cfg, err); if (!arguments.isEmpty()) addCommand(ExecArguments, KShell::joinArgs(arguments)); // Do other debugger specific config options and actually start the inferior program if (!execInferior(cfg, iexec, executable)) { return false; } QString config_startWith = cfg->config().readEntry(Config::StartWithEntry, QStringLiteral("ApplicationOutput")); if (config_startWith == QLatin1String("GdbConsole")) { emit raiseDebuggerConsoleViews(); } else if (config_startWith == QLatin1String("FrameStack")) { emit raiseFramestackViews(); } else { // ApplicationOutput is raised in DebugJob (by setting job to Verbose/Silent) } return true; } // FIXME: use same configuration process as startDebugging bool MIDebugSession::attachToProcess(int pid) { qCDebug(DEBUGGERCOMMON) << "Attach to process" << pid; emit showMessage(i18n("Attaching to process %1", pid), 1000); if (debuggerStateIsOn(s_dbgNotStarted)) { // FIXME: use global launch configuration rather than nullptr if (!startDebugger(nullptr)) { return false; } } setDebuggerStateOn(s_attached); //set current state to running, after attaching we will get *stopped response setDebuggerStateOn(s_appRunning); addCommand(TargetAttach, QString::number(pid), this, &MIDebugSession::handleTargetAttach, CmdHandlesError); addCommand(new SentinelCommand(breakpointController(), &MIBreakpointController::initSendBreakpoints)); raiseEvent(connected_to_program); emit raiseFramestackViews(); return true; } void MIDebugSession::handleTargetAttach(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { - KMessageBox::error( - qApp->activeWindow(), + const QString messageText = i18n("Could not attach debugger:
")+ - r[QStringLiteral("msg")].literal(), - i18n("Startup error")); + r[QStringLiteral("msg")].literal(); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); stopDebugger(); } } bool MIDebugSession::examineCoreFile(const QUrl &debugee, const QUrl &coreFile) { emit showMessage(i18n("Examining core file %1", coreFile.toLocalFile()), 1000); if (debuggerStateIsOn(s_dbgNotStarted)) { // FIXME: use global launch configuration rather than nullptr if (!startDebugger(nullptr)) { return false; } } // FIXME: support non-local URLs if (!loadCoreFile(nullptr, debugee.toLocalFile(), coreFile.toLocalFile())) { return false; } raiseEvent(program_state_changed); return true; } #define ENUM_NAME(o,e,v) (o::staticMetaObject.enumerator(o::staticMetaObject.indexOfEnumerator(#e)).valueToKey((v))) void MIDebugSession::setSessionState(DebuggerState state) { qCDebug(DEBUGGERCOMMON) << "Session state changed to" << ENUM_NAME(IDebugSession, DebuggerState, state) << "(" << state << ")"; if (state != m_sessionState) { m_sessionState = state; emit stateChanged(state); } } bool MIDebugSession::debuggerStateIsOn(DBGStateFlags state) const { return m_debuggerState & state; } DBGStateFlags MIDebugSession::debuggerState() const { return m_debuggerState; } void MIDebugSession::setDebuggerStateOn(DBGStateFlags stateOn) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, m_debuggerState | stateOn); m_debuggerState |= stateOn; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::setDebuggerStateOff(DBGStateFlags stateOff) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, m_debuggerState & ~stateOff); m_debuggerState &= ~stateOff; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::setDebuggerState(DBGStateFlags newState) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, newState); m_debuggerState = newState; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::debuggerStateChange(DBGStateFlags oldState, DBGStateFlags newState) { int delta = oldState ^ newState; if (delta) { QString out; #define STATE_CHECK(name) \ do { \ if (delta & name) { \ out += ((newState & name) ? QLatin1String(" +") : QLatin1String(" -")) \ + QLatin1String(#name); \ delta &= ~name; \ } \ } while (0) STATE_CHECK(s_dbgNotStarted); STATE_CHECK(s_appNotStarted); STATE_CHECK(s_programExited); STATE_CHECK(s_attached); STATE_CHECK(s_core); STATE_CHECK(s_shuttingDown); STATE_CHECK(s_dbgBusy); STATE_CHECK(s_appRunning); STATE_CHECK(s_dbgNotListening); STATE_CHECK(s_automaticContinue); #undef STATE_CHECK for (unsigned int i = 0; delta != 0 && i < 32; ++i) { if (delta & (1 << i)) { delta &= ~(1 << i); out += (((1 << i) & newState) ? QLatin1String(" +") : QLatin1String(" -")) + QString::number(i); } } } } void MIDebugSession::handleDebuggerStateChange(DBGStateFlags oldState, DBGStateFlags newState) { QString message; DebuggerState oldSessionState = state(); DebuggerState newSessionState = oldSessionState; DBGStateFlags changedState = oldState ^ newState; if (newState & s_dbgNotStarted) { if (changedState & s_dbgNotStarted) { message = i18n("Debugger stopped"); emit finished(); } if (oldSessionState != NotStartedState || newState & s_dbgFailedStart) { newSessionState = EndedState; } } else { if (newState & s_appNotStarted) { if (oldSessionState == NotStartedState || oldSessionState == StartingState) { newSessionState = StartingState; } else { newSessionState = StoppedState; } } else if (newState & s_programExited) { if (changedState & s_programExited) { message = i18n("Process exited"); } newSessionState = StoppedState; } else if (newState & s_appRunning) { if (changedState & s_appRunning) { message = i18n("Application is running"); } newSessionState = ActiveState; } else { if (changedState & s_appRunning) { message = i18n("Application is paused"); } newSessionState = PausedState; } } // And now? :-) qCDebug(DEBUGGERCOMMON) << "Debugger state changed to:" << newState << message << "- changes:" << changedState; if (!message.isEmpty()) emit showMessage(message, 3000); emit debuggerStateChanged(oldState, newState); // must be last, since it can lead to deletion of the DebugSession if (newSessionState != oldSessionState) { setSessionState(newSessionState); } } void MIDebugSession::restartDebugger() { // We implement restart as kill + slotRun, as opposed as plain "run" // command because kill + slotRun allows any special logic in slotRun // to apply for restart. // // That includes: // - checking for out-of-date project // - special setup for remote debugging. // // Had we used plain 'run' command, restart for remote debugging simply // would not work. if (!debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) { // FIXME: s_dbgBusy or m_debugger->isReady()? if (debuggerStateIsOn(s_dbgBusy)) { interruptDebugger(); } // The -exec-abort is not implemented in gdb // addCommand(ExecAbort); addCommand(NonMI, QStringLiteral("kill")); } run(); } void MIDebugSession::stopDebugger() { if (debuggerStateIsOn(s_dbgNotStarted)) { // we are force to stop even before debugger started, just reset qCDebug(DEBUGGERCOMMON) << "Stopping debugger when it's not started"; return; } m_commandQueue->clear(); qCDebug(DEBUGGERCOMMON) << "try stopping debugger"; if (debuggerStateIsOn(s_shuttingDown) || !m_debugger) return; setDebuggerStateOn(s_shuttingDown); qCDebug(DEBUGGERCOMMON) << "stopping debugger"; // Get debugger's attention if it's busy. We need debugger to be at the // command line so we can stop it. if (!m_debugger->isReady()) { qCDebug(DEBUGGERCOMMON) << "debugger busy on shutdown - interrupting"; interruptDebugger(); } // If the app is attached then we release it here. This doesn't stop // the app running. if (debuggerStateIsOn(s_attached)) { addCommand(TargetDetach); emit debuggerUserCommandOutput(QStringLiteral("(gdb) detach\n")); } // Now try to stop debugger running. addCommand(GdbExit); emit debuggerUserCommandOutput(QStringLiteral("(gdb) quit")); // We cannot wait forever, kill gdb after 5 seconds if it's not yet quit QTimer::singleShot(5000, this, [this]() { if (!debuggerStateIsOn(s_programExited) && debuggerStateIsOn(s_shuttingDown)) { qCDebug(DEBUGGERCOMMON) << "debugger not shutdown - killing"; m_debugger->kill(); setDebuggerState(s_dbgNotStarted | s_appNotStarted); raiseEvent(debugger_exited); } }); emit reset(); } void MIDebugSession::interruptDebugger() { Q_ASSERT(m_debugger); // Explicitly send the interrupt in case something went wrong with the usual // ensureGdbListening logic. m_debugger->interrupt(); addCommand(ExecInterrupt, QString(), CmdInterrupt); } void MIDebugSession::run() { if (debuggerStateIsOn(s_appNotStarted|s_dbgNotStarted|s_shuttingDown)) return; addCommand(MI::ExecContinue, QString(), CmdMaybeStartsRunning); } void MIDebugSession::runToCursor() { if (IDocument* doc = ICore::self()->documentController()->activeDocument()) { KTextEditor::Cursor cursor = doc->cursorPosition(); if (cursor.isValid()) runUntil(doc->url(), cursor.line() + 1); } } void MIDebugSession::jumpToCursor() { if (IDocument* doc = ICore::self()->documentController()->activeDocument()) { KTextEditor::Cursor cursor = doc->cursorPosition(); if (cursor.isValid()) jumpTo(doc->url(), cursor.line() + 1); } } void MIDebugSession::stepOver() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecNext, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepIntoInstruction() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecStepInstruction, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepInto() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecStep, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepOverInstruction() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecNextInstruction, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepOut() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecFinish, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::runUntil(const QUrl& url, int line) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!url.isValid()) { addCommand(ExecUntil, QString::number(line), CmdMaybeStartsRunning | CmdTemporaryRun); } else { addCommand(ExecUntil, QStringLiteral("%1:%2").arg(url.toLocalFile()).arg(line), CmdMaybeStartsRunning | CmdTemporaryRun); } } void MIDebugSession::runUntil(const QString& address) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!address.isEmpty()) { addCommand(ExecUntil, QStringLiteral("*%1").arg(address), CmdMaybeStartsRunning | CmdTemporaryRun); } } void MIDebugSession::jumpTo(const QUrl& url, int line) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (url.isValid()) { addCommand(NonMI, QStringLiteral("tbreak %1:%2").arg(url.toLocalFile()).arg(line)); addCommand(NonMI, QStringLiteral("jump %1:%2").arg(url.toLocalFile()).arg(line)); } } void MIDebugSession::jumpToMemoryAddress(const QString& address) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!address.isEmpty()) { addCommand(NonMI, QStringLiteral("tbreak *%1").arg(address)); addCommand(NonMI, QStringLiteral("jump *%1").arg(address)); } } void MIDebugSession::addUserCommand(const QString& cmd) { auto usercmd = createUserCommand(cmd); if (!usercmd) return; queueCmd(usercmd); // User command can theoretically modify absolutely everything, // so need to force a reload. // We can do it right now, and don't wait for user command to finish // since commands used to reload all view will be executed after // user command anyway. if (!debuggerStateIsOn(s_appNotStarted) && !debuggerStateIsOn(s_programExited)) raiseEvent(program_state_changed); } MICommand *MIDebugSession::createUserCommand(const QString &cmd) const { MICommand *res = nullptr; if (!cmd.isEmpty() && cmd[0].isDigit()) { // Add a space to the beginning, so debugger won't get confused if the // command starts with a number (won't mix it up with command token added) res = new UserCommand(MI::NonMI, QLatin1Char(' ') + cmd); } else { res = new UserCommand(MI::NonMI, cmd); } return res; } MICommand *MIDebugSession::createCommand(CommandType type, const QString& arguments, CommandFlags flags) const { return new MICommand(type, arguments, flags); } void MIDebugSession::addCommand(MICommand* cmd) { queueCmd(cmd); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, MI::CommandFlags flags) { queueCmd(createCommand(type, arguments, flags)); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, MI::MICommandHandler *handler, MI::CommandFlags flags) { auto cmd = createCommand(type, arguments, flags); cmd->setHandler(handler); queueCmd(cmd); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, const MI::FunctionCommandHandler::Function& callback, MI::CommandFlags flags) { auto cmd = createCommand(type, arguments, flags); cmd->setHandler(callback); queueCmd(cmd); } // Fairly obvious that we'll add whatever command you give me to a queue // Not quite so obvious though is that if we are going to run again. then any // information requests become redundent and must be removed. // We also try and run whatever command happens to be at the head of // the queue. void MIDebugSession::queueCmd(MICommand *cmd) { if (debuggerStateIsOn(s_dbgNotStarted)) { - KMessageBox::information( - qApp->activeWindow(), + const QString messageText = i18n("Gdb command sent when debugger is not running
" - "The command was:
%1", cmd->initialString()), - i18n("Internal error")); + "The command was:
%1", cmd->initialString()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Information); + ICore::self()->uiController()->postMessage(message); return; } if (m_stateReloadInProgress) cmd->setStateReloading(true); m_commandQueue->enqueue(cmd); qCDebug(DEBUGGERCOMMON) << "QUEUE: " << cmd->initialString() << (m_stateReloadInProgress ? "(state reloading)" : "") << m_commandQueue->count() << "pending"; bool varCommandWithContext= (cmd->type() >= MI::VarAssign && cmd->type() <= MI::VarUpdate && cmd->type() != MI::VarDelete); bool stackCommandWithContext = (cmd->type() >= MI::StackInfoDepth && cmd->type() <= MI::StackListLocals); if (varCommandWithContext || stackCommandWithContext) { if (cmd->thread() == -1) qCDebug(DEBUGGERCOMMON) << "\t--thread will be added on execution"; if (cmd->frame() == -1) qCDebug(DEBUGGERCOMMON) << "\t--frame will be added on execution"; } setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_busy); executeCmd(); } void MIDebugSession::executeCmd() { Q_ASSERT(m_debugger); if (debuggerStateIsOn(s_dbgNotListening) && m_commandQueue->haveImmediateCommand()) { // We may have to call this even while a command is currently executing, because // debugger can get into a state where a command such as ExecRun does not send a response // while the inferior is running. ensureDebuggerListening(); } if (!m_debugger->isReady()) return; MICommand* currentCmd = m_commandQueue->nextCommand(); if (!currentCmd) return; if (currentCmd->flags() & (CmdMaybeStartsRunning | CmdInterrupt)) { setDebuggerStateOff(s_automaticContinue); } if (currentCmd->flags() & CmdMaybeStartsRunning) { // GDB can be in a state where it is listening for commands while the program is running. // However, when we send a command such as ExecContinue in this state, GDB may return to // the non-listening state without acknowledging that the ExecContinue command has even // finished, let alone sending a new notification about the program's running state. // So let's be extra cautious about ensuring that we will wake GDB up again if required. setDebuggerStateOn(s_dbgNotListening); } bool varCommandWithContext= (currentCmd->type() >= MI::VarAssign && currentCmd->type() <= MI::VarUpdate && currentCmd->type() != MI::VarDelete); bool stackCommandWithContext = (currentCmd->type() >= MI::StackInfoDepth && currentCmd->type() <= MI::StackListLocals); if (varCommandWithContext || stackCommandWithContext) { // Most var commands should be executed in the context // of the selected thread and frame. if (currentCmd->thread() == -1) currentCmd->setThread(frameStackModel()->currentThread()); if (currentCmd->frame() == -1) currentCmd->setFrame(frameStackModel()->currentFrame()); } QString commandText = currentCmd->cmdToSend(); bool bad_command = false; QString message; int length = commandText.length(); // No i18n for message since it's mainly for debugging. if (length == 0) { // The command might decide it's no longer necessary to send // it. if (auto* sc = dynamic_cast(currentCmd)) { qCDebug(DEBUGGERCOMMON) << "SEND: sentinel command, not sending"; sc->invokeHandler(); } else { qCDebug(DEBUGGERCOMMON) << "SEND: command " << currentCmd->initialString() << "changed its mind, not sending"; } delete currentCmd; executeCmd(); return; } else { if (commandText[length-1] != QLatin1Char('\n')) { bad_command = true; message = QStringLiteral("Debugger command does not end with newline"); } } if (bad_command) { - KMessageBox::information(qApp->activeWindow(), - i18n("Invalid debugger command
%1", message), - i18n("Invalid debugger command")); + const QString messageText = i18n("Invalid debugger command
%1", message); + auto* message = new Sublime::Message(messageText, Sublime::Message::Information); + ICore::self()->uiController()->postMessage(message); executeCmd(); return; } m_debugger->execute(currentCmd); } void MIDebugSession::ensureDebuggerListening() { Q_ASSERT(m_debugger); // Note: we don't use interruptDebugger() here since // we don't want to queue more commands before queuing a command m_debugger->interrupt(); setDebuggerStateOn(s_interruptSent); if (debuggerStateIsOn(s_appRunning)) setDebuggerStateOn(s_automaticContinue); setDebuggerStateOff(s_dbgNotListening); } void MIDebugSession::destroyCmds() { m_commandQueue->clear(); } // FIXME: I don't fully remember what is the business with // m_stateReloadInProgress and whether we can lift it to the // generic level. void MIDebugSession::raiseEvent(event_t e) { if (e == program_exited || e == debugger_exited) { m_stateReloadInProgress = false; } if (e == program_state_changed) { m_stateReloadInProgress = true; qCDebug(DEBUGGERCOMMON) << "State reload in progress\n"; } IDebugSession::raiseEvent(e); if (e == program_state_changed) { m_stateReloadInProgress = false; } } bool KDevMI::MIDebugSession::hasCrashed() const { return m_hasCrashed; } void MIDebugSession::slotDebuggerReady() { Q_ASSERT(m_debugger); m_stateReloadInProgress = false; executeCmd(); if (m_debugger->isReady()) { /* There is nothing in the command queue and no command is currently executing. */ if (debuggerStateIsOn(s_automaticContinue)) { if (!debuggerStateIsOn(s_appRunning)) { qCDebug(DEBUGGERCOMMON) << "Posting automatic continue"; addCommand(ExecContinue, QString(), CmdMaybeStartsRunning); } setDebuggerStateOff(s_automaticContinue); return; } if (m_stateReloadNeeded && !debuggerStateIsOn(s_appRunning)) { qCDebug(DEBUGGERCOMMON) << "Finishing program stop"; // Set to false right now, so that if 'actOnProgramPauseMI_part2' // sends some commands, we won't call it again when handling replies // from that commands. m_stateReloadNeeded = false; reloadProgramState(); } qCDebug(DEBUGGERCOMMON) << "No more commands"; setDebuggerStateOff(s_dbgBusy); raiseEvent(debugger_ready); } } void MIDebugSession::slotDebuggerExited(bool abnormal, const QString &msg) { /* Technically speaking, GDB is likely not to kill the application, and we should have some backup mechanism to make sure the application is killed by KDevelop. But even if application stays around, we no longer can control it in any way, so mark it as exited. */ setDebuggerStateOn(s_appNotStarted); setDebuggerStateOn(s_dbgNotStarted); setDebuggerStateOn(s_programExited); setDebuggerStateOff(s_shuttingDown); if (!msg.isEmpty()) emit showMessage(msg, 3000); if (abnormal) { /* The error is reported to user in MIDebugger now. KMessageBox::information( KDevelop::ICore::self()->uiController()->activeMainWindow(), i18n("Debugger exited abnormally" "

This is likely a bug in GDB. " "Examine the gdb output window and then stop the debugger"), i18n("Debugger exited abnormally")); */ // FIXME: not sure if the following still applies. // Note: we don't stop the debugger here, becuse that will hide gdb // window and prevent the user from finding the exact reason of the // problem. } /* FIXME: raiseEvent is handled across multiple places where we explicitly * stop/kill the debugger, a better way is to let the debugger itself report * its exited event. */ // raiseEvent(debugger_exited); } void MIDebugSession::slotInferiorStopped(const MI::AsyncRecord& r) { /* By default, reload all state on program stop. */ m_stateReloadNeeded = true; setDebuggerStateOff(s_appRunning); setDebuggerStateOff(s_dbgNotListening); QString reason; if (r.hasField(QStringLiteral("reason"))) reason = r[QStringLiteral("reason")].literal(); if (reason == QLatin1String("exited-normally") || reason == QLatin1String("exited")) { if (r.hasField(QStringLiteral("exit-code"))) { programNoApp(i18n("Exited with return code: %1", r[QStringLiteral("exit-code")].literal())); } else { programNoApp(i18n("Exited normally")); } m_stateReloadNeeded = false; return; } if (reason == QLatin1String("exited-signalled")) { programNoApp(i18n("Exited on signal %1", r[QStringLiteral("signal-name")].literal())); m_stateReloadNeeded = false; return; } if (reason == QLatin1String("watchpoint-scope")) { // FIXME: should remove this watchpoint // But first, we should consider if removing all // watchpoints on program exit is the right thing to // do. addCommand(ExecContinue, QString(), CmdMaybeStartsRunning); m_stateReloadNeeded = false; return; } bool wasInterrupt = false; if (reason == QLatin1String("signal-received")) { QString name = r[QStringLiteral("signal-name")].literal(); QString user_name = r[QStringLiteral("signal-meaning")].literal(); // SIGINT is a "break into running program". // We do this when the user set/mod/clears a breakpoint but the // application is running. // And the user does this to stop the program also. if (name == QLatin1String("SIGINT") && debuggerStateIsOn(s_interruptSent)) { wasInterrupt = true; } else { // Whenever we have a signal raised then tell the user, but don't // end the program as we want to allow the user to look at why the // program has a signal that's caused the prog to stop. // Continuing from SIG FPE/SEGV will cause a "Cannot ..." and // that'll end the program. programFinished(i18n("Program received signal %1 (%2)", name, user_name)); m_hasCrashed = true; } } if (!reason.contains(QLatin1String("exited"))) { // FIXME: we should immediately update the current thread and // frame in the framestackmodel, so that any user actions // are in that thread. However, the way current framestack model // is implemented, we can't change thread id until we refresh // the entire list of threads -- otherwise we might set a thread // id that is not already in the list, and it will be upset. //Indicates if program state should be reloaded immediately. bool updateState = false; if (r.hasField(QStringLiteral("frame"))) { const MI::Value& frame = r[QStringLiteral("frame")]; QString file, line, addr; if (frame.hasField(QStringLiteral("fullname"))) file = frame[QStringLiteral("fullname")].literal(); if (frame.hasField(QStringLiteral("line"))) line = frame[QStringLiteral("line")].literal(); if (frame.hasField(QStringLiteral("addr"))) addr = frame[QStringLiteral("addr")].literal(); // gdb counts lines from 1 and we don't setCurrentPosition(QUrl::fromLocalFile(file), line.toInt() - 1, addr); updateState = true; } if (updateState) { reloadProgramState(); } } setDebuggerStateOff(s_interruptSent); if (!wasInterrupt) setDebuggerStateOff(s_automaticContinue); } void MIDebugSession::slotInferiorRunning() { setDebuggerStateOn(s_appRunning); raiseEvent(program_running); if (m_commandQueue->haveImmediateCommand() || (m_debugger->currentCommand() && (m_debugger->currentCommand()->flags() & (CmdImmediately | CmdInterrupt)))) { ensureDebuggerListening(); } else { setDebuggerStateOn(s_dbgNotListening); } } void MIDebugSession::processNotification(const MI::AsyncRecord & async) { if (async.reason == QLatin1String("thread-group-started")) { setDebuggerStateOff(s_appNotStarted | s_programExited); } else if (async.reason == QLatin1String("thread-group-exited")) { setDebuggerStateOn(s_programExited); } else if (async.reason == QLatin1String("library-loaded")) { // do nothing } else if (async.reason == QLatin1String("breakpoint-created")) { breakpointController()->notifyBreakpointCreated(async); } else if (async.reason == QLatin1String("breakpoint-modified")) { breakpointController()->notifyBreakpointModified(async); } else if (async.reason == QLatin1String("breakpoint-deleted")) { breakpointController()->notifyBreakpointDeleted(async); } else { qCDebug(DEBUGGERCOMMON) << "Unhandled notification: " << async.reason; } } void MIDebugSession::reloadProgramState() { raiseEvent(program_state_changed); m_stateReloadNeeded = false; } // There is no app anymore. This can be caused by program exiting // an invalid program specified or ... // gdb is still running though, but only the run command (may) make sense // all other commands are disabled. void MIDebugSession::programNoApp(const QString& msg) { qCDebug(DEBUGGERCOMMON) << msg; setDebuggerState(s_appNotStarted | s_programExited | (m_debuggerState & s_shuttingDown)); destroyCmds(); // The application has existed, but it's possible that // some of application output is still in the pipe. We use // different pipes to communicate with gdb and to get application // output, so "exited" message from gdb might have arrived before // last application output. Get this last bit. // Note: this method can be called when we open an invalid // core file. In that case, tty_ won't be set. if (m_tty){ m_tty->readRemaining(); // Tty is no longer usable, delete it. Without this, QSocketNotifier // will continuously bomd STTY with signals, so we need to either disable // QSocketNotifier, or delete STTY. The latter is simpler, since we can't // reuse it for future debug sessions anyway. m_tty.reset(nullptr); } stopDebugger(); raiseEvent(program_exited); raiseEvent(debugger_exited); emit showMessage(msg, 0); programFinished(msg); } void MIDebugSession::programFinished(const QString& msg) { QString m = QStringLiteral("*** %0 ***").arg(msg.trimmed()); emit inferiorStderrLines(QStringList(m)); /* Also show message in gdb window, so that users who prefer to look at gdb window know what's up. */ emit debuggerUserCommandOutput(m); } void MIDebugSession::explainDebuggerStatus() { MICommand* currentCmd_ = m_debugger->currentCommand(); QString information = i18np("1 command in queue\n", "%1 commands in queue\n", m_commandQueue->count()) + i18ncp("Only the 0 and 1 cases need to be translated", "1 command being processed by gdb\n", "%1 commands being processed by gdb\n", (currentCmd_ ? 1 : 0)) + i18n("Debugger state: %1\n", m_debuggerState); if (currentCmd_) { QString extra = i18n("Current command class: '%1'\n" "Current command text: '%2'\n" "Current command original text: '%3'\n", QString::fromUtf8(typeid(*currentCmd_).name()), currentCmd_->cmdToSend(), currentCmd_->initialString()); information += extra; } - KMessageBox::information(qApp->activeWindow(), information, - i18n("Debugger status")); + auto* message = new Sublime::Message(information, Sublime::Message::Information); + ICore::self()->uiController()->postMessage(message); } // There is no app anymore. This can be caused by program exiting // an invalid program specified or ... // gdb is still running though, but only the run command (may) make sense // all other commands are disabled. void MIDebugSession::handleNoInferior(const QString& msg) { qCDebug(DEBUGGERCOMMON) << msg; setDebuggerState(s_appNotStarted | s_programExited | (debuggerState() & s_shuttingDown)); destroyCmds(); // The application has existed, but it's possible that // some of application output is still in the pipe. We use // different pipes to communicate with gdb and to get application // output, so "exited" message from gdb might have arrived before // last application output. Get this last bit. // Note: this method can be called when we open an invalid // core file. In that case, tty_ won't be set. if (m_tty){ m_tty->readRemaining(); // Tty is no longer usable, delete it. Without this, QSocketNotifier // will continuously bomd STTY with signals, so we need to either disable // QSocketNotifier, or delete STTY. The latter is simpler, since we can't // reuse it for future debug sessions anyway. m_tty.reset(nullptr); } stopDebugger(); raiseEvent(program_exited); raiseEvent(debugger_exited); emit showMessage(msg, 0); handleInferiorFinished(msg); } void MIDebugSession::handleInferiorFinished(const QString& msg) { QString m = QStringLiteral("*** %0 ***").arg(msg.trimmed()); emit inferiorStderrLines(QStringList(m)); /* Also show message in gdb window, so that users who prefer to look at gdb window know what's up. */ emit debuggerUserCommandOutput(m); } // FIXME: connect to debugger's slot. void MIDebugSession::defaultErrorHandler(const MI::ResultRecord& result) { QString msg = result[QStringLiteral("msg")].literal(); if (msg.contains(QLatin1String("No such process"))) { setDebuggerState(s_appNotStarted|s_programExited); raiseEvent(program_exited); return; } - KMessageBox::information( - qApp->activeWindow(), + const QString messageText = i18n("Debugger error" "

Debugger reported the following error:" - "

%1", result[QStringLiteral("msg")].literal()), - i18n("Debugger error")); + "

%1", result[QStringLiteral("msg")].literal()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); // Error most likely means that some change made in GUI // was not communicated to the gdb, so GUI is now not // in sync with gdb. Resync it. // // Another approach is to make each widget reload it content // on errors from commands that it sent, but that's too complex. // Errors are supposed to happen rarely, so full reload on error // is not a big deal. Well, maybe except for memory view, but // it's no auto-reloaded anyway. // // Also, don't reload state on errors appeared during state // reloading! if (!m_debugger->currentCommand()->stateReloading()) raiseEvent(program_state_changed); } void MIDebugSession::setSourceInitFile(bool enable) { m_sourceInitFile = enable; } diff --git a/plugins/execute/executeplugin.cpp b/plugins/execute/executeplugin.cpp index 01ee091549..be14941620 100644 --- a/plugins/execute/executeplugin.cpp +++ b/plugins/execute/executeplugin.cpp @@ -1,247 +1,248 @@ /* * This file is part of KDevelop * * 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 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 "executeplugin.h" #include #include #include -#include #include #include #include #include #include #include #include #include +#include #include "nativeappconfig.h" #include "debug.h" #include #include #include QString ExecutePlugin::_nativeAppConfigTypeId = QStringLiteral("Native Application"); QString ExecutePlugin::workingDirEntry = QStringLiteral("Working Directory"); QString ExecutePlugin::executableEntry = QStringLiteral("Executable"); QString ExecutePlugin::argumentsEntry = QStringLiteral("Arguments"); QString ExecutePlugin::isExecutableEntry = QStringLiteral("isExecutable"); QString ExecutePlugin::dependencyEntry = QStringLiteral("Dependencies"); // TODO: migrate to more consistent key term "EnvironmentProfile" QString ExecutePlugin::environmentProfileEntry = QStringLiteral("EnvironmentGroup"); QString ExecutePlugin::useTerminalEntry = QStringLiteral("Use External Terminal"); QString ExecutePlugin::terminalEntry = QStringLiteral("External Terminal"); QString ExecutePlugin::userIdToRunEntry = QStringLiteral("User Id to Run"); QString ExecutePlugin::dependencyActionEntry = QStringLiteral("Dependency Action"); QString ExecutePlugin::projectTargetEntry = QStringLiteral("Project Target"); using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(KDevExecuteFactory, "kdevexecute.json", registerPlugin();) ExecutePlugin::ExecutePlugin(QObject *parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevexecute"), parent) { m_configType = new NativeAppConfigType(); m_configType->addLauncher( new NativeAppLauncher() ); qCDebug(PLUGIN_EXECUTE) << "adding native app launch config"; core()->runController()->addConfigurationType( m_configType ); } ExecutePlugin::~ExecutePlugin() { } void ExecutePlugin::unload() { core()->runController()->removeConfigurationType( m_configType ); delete m_configType; m_configType = nullptr; } QStringList ExecutePlugin::arguments( KDevelop::ILaunchConfiguration* cfg, QString& err_ ) const { if( !cfg ) { return QStringList(); } KShell::Errors err; QStringList args = KShell::splitArgs( cfg->config().readEntry( ExecutePlugin::argumentsEntry, "" ), KShell::TildeExpand | KShell::AbortOnMeta, &err ); if( err != KShell::NoError ) { if( err == KShell::BadQuoting ) { err_ = i18n("There is a quoting error in the arguments for " "the launch configuration '%1'. Aborting start.", cfg->name() ); } else { err_ = i18n("A shell meta character was included in the " "arguments for the launch configuration '%1', " "this is not supported currently. Aborting start.", cfg->name() ); } args = QStringList(); qCWarning(PLUGIN_EXECUTE) << "Launch Configuration:" << cfg->name() << "arguments have meta characters"; } return args; } KJob* ExecutePlugin::dependencyJob( KDevelop::ILaunchConfiguration* cfg ) const { const QVariantList deps = KDevelop::stringToQVariant( cfg->config().readEntry( dependencyEntry, QString() ) ).toList(); QString depAction = cfg->config().readEntry( dependencyActionEntry, "Nothing" ); if( depAction != QLatin1String("Nothing") && !deps.isEmpty() ) { KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel(); QList items; for (const QVariant& dep : deps) { KDevelop::ProjectBaseItem* item = model->itemFromIndex( model->pathToIndex( dep.toStringList() ) ); if( item ) { items << item; } else { - KMessageBox::error(core()->uiController()->activeMainWindow(), - i18n("Couldn't resolve the dependency: %1", dep.toString())); + const QString messageText = i18n("Couldn't resolve the dependency: %1", dep.toString()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); } } auto* job = new KDevelop::BuilderJob(); if( depAction == QLatin1String("Build") ) { job->addItems( KDevelop::BuilderJob::Build, items ); } else if( depAction == QLatin1String("Install") ) { job->addItems( KDevelop::BuilderJob::Install, items ); } job->updateJobName(); return job; } return nullptr; } QString ExecutePlugin::environmentProfileName(KDevelop::ILaunchConfiguration* cfg) const { if( !cfg ) { return QString(); } return cfg->config().readEntry(ExecutePlugin::environmentProfileEntry, QString()); } QUrl ExecutePlugin::executable( KDevelop::ILaunchConfiguration* cfg, QString& err ) const { QUrl executable; if( !cfg ) { return executable; } KConfigGroup grp = cfg->config(); if( grp.readEntry(ExecutePlugin::isExecutableEntry, false ) ) { executable = grp.readEntry( ExecutePlugin::executableEntry, QUrl() ); } else { QStringList prjitem = grp.readEntry( ExecutePlugin::projectTargetEntry, QStringList() ); KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel(); KDevelop::ProjectBaseItem* item = model->itemFromIndex( model->pathToIndex(prjitem) ); if( item && item->executable() ) { // TODO: Need an option in the gui to choose between installed and builddir url here, currently cmake only supports builddir url executable = item->executable()->builtUrl(); } } if( executable.isEmpty() ) { err = i18n("No valid executable specified"); qCWarning(PLUGIN_EXECUTE) << "Launch Configuration:" << cfg->name() << "no valid executable set"; } else { KShell::Errors err_; if( KShell::splitArgs( executable.toLocalFile(), KShell::TildeExpand | KShell::AbortOnMeta, &err_ ).isEmpty() || err_ != KShell::NoError ) { executable = QUrl(); if( err_ == KShell::BadQuoting ) { err = i18n("There is a quoting error in the executable " "for the launch configuration '%1'. " "Aborting start.", cfg->name() ); } else { err = i18n("A shell meta character was included in the " "executable for the launch configuration '%1', " "this is not supported currently. Aborting start.", cfg->name() ); } qCWarning(PLUGIN_EXECUTE) << "Launch Configuration:" << cfg->name() << "executable has meta characters"; } } return executable; } bool ExecutePlugin::useTerminal( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return false; } return cfg->config().readEntry( ExecutePlugin::useTerminalEntry, false ); } QString ExecutePlugin::terminal( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return QString(); } return cfg->config().readEntry( ExecutePlugin::terminalEntry, QString() ); } QUrl ExecutePlugin::workingDirectory( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return QUrl(); } return cfg->config().readEntry( ExecutePlugin::workingDirEntry, QUrl() ); } QString ExecutePlugin::nativeAppConfigTypeId() const { return _nativeAppConfigTypeId; } #include "executeplugin.moc" diff --git a/plugins/externalscript/CMakeLists.txt b/plugins/externalscript/CMakeLists.txt index 66fa3aba16..a3d3fd9d0c 100644 --- a/plugins/externalscript/CMakeLists.txt +++ b/plugins/externalscript/CMakeLists.txt @@ -1,32 +1,39 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevexternalscript\") ########### next target ############### set(kdevexternalscript_PART_SRCS externalscriptplugin.cpp externalscriptview.cpp externalscriptitem.cpp externalscriptjob.cpp editexternalscript.cpp ) declare_qt_logging_category(kdevexternalscript_PART_SRCS TYPE PLUGIN IDENTIFIER PLUGIN_EXTERNALSCRIPT CATEGORY_BASENAME "externalscript" ) set(kdevexternalscript_PART_UI externalscriptview.ui editexternalscript.ui ) ki18n_wrap_ui(kdevexternalscript_PART_SRCS ${kdevexternalscript_PART_UI}) qt5_add_resources(kdevexternalscript_PART_SRCS kdevexternalscript.qrc) kdevplatform_add_plugin(kdevexternalscript JSON kdevexternalscript.json SOURCES ${kdevexternalscript_PART_SRCS}) target_link_libraries(kdevexternalscript - KF5::TextEditor KF5::KIOWidgets KF5::Parts KF5::NewStuff - KDev::Language KDev::Interfaces KDev::Project - KDev::Util KDev::OutputView + KDev::Language + KDev::Interfaces + KDev::Project + KDev::Sublime + KDev::Util + KDev::OutputView + KF5::TextEditor + KF5::KIOWidgets + KF5::Parts + KF5::NewStuff ) diff --git a/plugins/externalscript/externalscriptjob.cpp b/plugins/externalscript/externalscriptjob.cpp index f69b45fd08..d1ee344555 100644 --- a/plugins/externalscript/externalscriptjob.cpp +++ b/plugins/externalscript/externalscriptjob.cpp @@ -1,410 +1,411 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat Copyright 2010 Milian Wolff 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 "externalscriptjob.h" #include "externalscriptitem.h" #include "externalscriptplugin.h" #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include +#include #include #include +#include #include using namespace KDevelop; ExternalScriptJob::ExternalScriptJob(ExternalScriptItem* item, const QUrl& url, ExternalScriptPlugin* parent) : KDevelop::OutputJob(parent) , m_proc(nullptr) , m_lineMaker(nullptr) , m_outputMode(item->outputMode()) , m_inputMode(item->inputMode()) , m_errorMode(item->errorMode()) , m_filterMode(item->filterMode()) , m_document(nullptr) , m_url(url) , m_selectionRange(KTextEditor::Range::invalid()) , m_showOutput(item->showOutput()) { qCDebug(PLUGIN_EXTERNALSCRIPT) << "creating external script job"; setCapabilities(Killable); setStandardToolView(KDevelop::IOutputView::RunView); setBehaviours(KDevelop::IOutputView::AllowUserClose | KDevelop::IOutputView::AutoScroll); auto* model = new KDevelop::OutputModel; model->setFilteringStrategy(static_cast(m_filterMode)); setModel(model); setDelegate(new KDevelop::OutputDelegate); // also merge when error mode "equals" output mode if ((m_outputMode == ExternalScriptItem::OutputInsertAtCursor && m_errorMode == ExternalScriptItem::ErrorInsertAtCursor) || (m_outputMode == ExternalScriptItem::OutputReplaceDocument && m_errorMode == ExternalScriptItem::ErrorReplaceDocument) || (m_outputMode == ExternalScriptItem::OutputReplaceSelectionOrDocument && m_errorMode == ExternalScriptItem::ErrorReplaceSelectionOrDocument) || (m_outputMode == ExternalScriptItem::OutputReplaceSelectionOrInsertAtCursor && m_errorMode == ExternalScriptItem::ErrorReplaceSelectionOrInsertAtCursor) || // also these two otherwise they clash... (m_outputMode == ExternalScriptItem::OutputReplaceSelectionOrInsertAtCursor && m_errorMode == ExternalScriptItem::ErrorReplaceSelectionOrDocument) || (m_outputMode == ExternalScriptItem::OutputReplaceSelectionOrDocument && m_errorMode == ExternalScriptItem::ErrorReplaceSelectionOrInsertAtCursor)) { m_errorMode = ExternalScriptItem::ErrorMergeOutput; } KTextEditor::View* view = KDevelop::ICore::self()->documentController()->activeTextDocumentView(); if (m_outputMode != ExternalScriptItem::OutputNone || m_inputMode != ExternalScriptItem::InputNone || m_errorMode != ExternalScriptItem::ErrorNone) { if (!view) { - KMessageBox::error(QApplication::activeWindow(), - i18n("Cannot run script '%1' since it tries to access " - "the editor contents but no document is open.", item->text()), - i18n("No Document Open") - ); + const QString messageText = + i18n("Cannot run script '%1' since it tries to access " + "the editor contents but no document is open.", item->text()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); return; } m_document = view->document(); connect(m_document, &KTextEditor::Document::aboutToClose, this, [&] { kill(); }); m_selectionRange = view->selectionRange(); m_cursorPosition = view->cursorPosition(); } if (item->saveMode() == ExternalScriptItem::SaveCurrentDocument && view) { view->document()->save(); } else if (item->saveMode() == ExternalScriptItem::SaveAllDocuments) { const auto documents = KDevelop::ICore::self()->documentController()->openDocuments(); for (KDevelop::IDocument* doc : documents) { doc->save(); } } QString command = item->command(); QString workingDir = item->workingDirectory(); if (item->performParameterReplacement()) command.replace(QLatin1String("%i"), QString::number(QCoreApplication::applicationPid())); if (!m_url.isEmpty()) { const QUrl url = m_url; KDevelop::ProjectFolderItem* folder = nullptr; if (KDevelop::ICore::self()->projectController()->findProjectForUrl(url)) { QList folders = KDevelop::ICore::self()->projectController()->findProjectForUrl(url)->foldersForPath(KDevelop::IndexedString( url)); if (!folders.isEmpty()) { folder = folders.first(); } } if (folder) { if (folder->path().isLocalFile() && workingDir.isEmpty()) { ///TODO: make configurable, use fallback to project dir workingDir = folder->path().toLocalFile(); } ///TODO: make those placeholders escapeable if (item->performParameterReplacement()) { command.replace(QLatin1String("%d"), KShell::quoteArg(m_url.toString(QUrl::PreferLocalFile))); if (KDevelop::IProject* project = KDevelop::ICore::self()->projectController()->findProjectForUrl(m_url)) { command.replace(QLatin1String("%p"), project->path().pathOrUrl()); } } } else { if (m_url.isLocalFile() && workingDir.isEmpty()) { ///TODO: make configurable, use fallback to project dir workingDir = view->document()->url().adjusted(QUrl::RemoveFilename).toLocalFile(); } ///TODO: make those placeholders escapeable if (item->performParameterReplacement()) { command.replace(QLatin1String("%u"), KShell::quoteArg(m_url.toString())); ///TODO: does that work with remote files? QFileInfo info(m_url.toString(QUrl::PreferLocalFile)); command.replace(QLatin1String("%f"), KShell::quoteArg(info.filePath())); command.replace(QLatin1String("%b"), KShell::quoteArg(info.baseName())); command.replace(QLatin1String("%n"), KShell::quoteArg(info.fileName())); command.replace(QLatin1String("%d"), KShell::quoteArg(info.path())); if (view->document()) { command.replace(QLatin1String("%c"), KShell::quoteArg(QString::number(view->cursorPosition().column()))); command.replace(QLatin1String("%l"), KShell::quoteArg(QString::number( view->cursorPosition().line()))); } if (view->document() && view->selection()) { command.replace(QLatin1String("%s"), KShell::quoteArg(view->selectionText())); } if (KDevelop::IProject* project = KDevelop::ICore::self()->projectController()->findProjectForUrl(m_url)) { command.replace(QLatin1String("%p"), project->path().pathOrUrl()); } } } } m_proc = new KProcess(this); if (!workingDir.isEmpty()) { m_proc->setWorkingDirectory(workingDir); } m_lineMaker = new ProcessLineMaker(m_proc, this); connect(m_lineMaker, &ProcessLineMaker::receivedStdoutLines, model, &OutputModel::appendLines); connect(m_lineMaker, &ProcessLineMaker::receivedStdoutLines, this, &ExternalScriptJob::receivedStdoutLines); connect(m_lineMaker, &ProcessLineMaker::receivedStderrLines, model, &OutputModel::appendLines); connect(m_lineMaker, &ProcessLineMaker::receivedStderrLines, this, &ExternalScriptJob::receivedStderrLines); connect(m_proc, &QProcess::errorOccurred, this, &ExternalScriptJob::processError); connect(m_proc, QOverload::of(&QProcess::finished), this, &ExternalScriptJob::processFinished); // Now setup the process parameters qCDebug(PLUGIN_EXTERNALSCRIPT) << "setting command:" << command; if (m_errorMode == ExternalScriptItem::ErrorMergeOutput) { m_proc->setOutputChannelMode(KProcess::MergedChannels); } else { m_proc->setOutputChannelMode(KProcess::SeparateChannels); } m_proc->setShellCommand(command); setObjectName(command); } void ExternalScriptJob::start() { qCDebug(PLUGIN_EXTERNALSCRIPT) << "launching?" << m_proc; if (m_proc) { if (m_showOutput) { startOutput(); } appendLine(i18n("Running external script: %1", m_proc->program().join(QLatin1Char(' ')))); m_proc->start(); if (m_inputMode != ExternalScriptItem::InputNone) { QString inputText; switch (m_inputMode) { case ExternalScriptItem::InputNone: // do nothing; break; case ExternalScriptItem::InputSelectionOrNone: if (m_selectionRange.isValid()) { inputText = m_document->text(m_selectionRange); } // else nothing break; case ExternalScriptItem::InputSelectionOrDocument: if (m_selectionRange.isValid()) { inputText = m_document->text(m_selectionRange); } else { inputText = m_document->text(); } break; case ExternalScriptItem::InputDocument: inputText = m_document->text(); break; } ///TODO: what to do with the encoding here? /// maybe ask Christoph for what kate returns... m_proc->write(inputText.toUtf8()); m_proc->closeWriteChannel(); } } else { qCWarning(PLUGIN_EXTERNALSCRIPT) << "No process, something went wrong when creating the job"; // No process means we've returned early on from the constructor, some bad error happened emitResult(); } } bool ExternalScriptJob::doKill() { if (m_proc) { m_proc->kill(); appendLine(i18n("*** Killed Application ***")); } return true; } void ExternalScriptJob::processFinished(int exitCode, QProcess::ExitStatus status) { m_lineMaker->flushBuffers(); if (exitCode == 0 && status == QProcess::NormalExit) { if (m_outputMode != ExternalScriptItem::OutputNone) { if (!m_stdout.isEmpty()) { QString output = m_stdout.join(QLatin1Char('\n')); switch (m_outputMode) { case ExternalScriptItem::OutputNone: // do nothing; break; case ExternalScriptItem::OutputCreateNewFile: KDevelop::ICore::self()->documentController()->openDocumentFromText(output); break; case ExternalScriptItem::OutputInsertAtCursor: m_document->insertText(m_cursorPosition, output); break; case ExternalScriptItem::OutputReplaceSelectionOrInsertAtCursor: if (m_selectionRange.isValid()) { m_document->replaceText(m_selectionRange, output); } else { m_document->insertText(m_cursorPosition, output); } break; case ExternalScriptItem::OutputReplaceSelectionOrDocument: if (m_selectionRange.isValid()) { m_document->replaceText(m_selectionRange, output); } else { m_document->setText(output); } break; case ExternalScriptItem::OutputReplaceDocument: m_document->setText(output); break; } } } if (m_errorMode != ExternalScriptItem::ErrorNone && m_errorMode != ExternalScriptItem::ErrorMergeOutput) { QString output = m_stderr.join(QLatin1Char('\n')); if (!output.isEmpty()) { switch (m_errorMode) { case ExternalScriptItem::ErrorNone: case ExternalScriptItem::ErrorMergeOutput: // do nothing; break; case ExternalScriptItem::ErrorCreateNewFile: KDevelop::ICore::self()->documentController()->openDocumentFromText(output); break; case ExternalScriptItem::ErrorInsertAtCursor: m_document->insertText(m_cursorPosition, output); break; case ExternalScriptItem::ErrorReplaceSelectionOrInsertAtCursor: if (m_selectionRange.isValid()) { m_document->replaceText(m_selectionRange, output); } else { m_document->insertText(m_cursorPosition, output); } break; case ExternalScriptItem::ErrorReplaceSelectionOrDocument: if (m_selectionRange.isValid()) { m_document->replaceText(m_selectionRange, output); } else { m_document->setText(output); } break; case ExternalScriptItem::ErrorReplaceDocument: m_document->setText(output); break; } } } appendLine(i18n("*** Exited normally ***")); } else { if (status == QProcess::NormalExit) appendLine(i18n("*** Exited with return code: %1 ***", QString::number(exitCode))); else if (error() == KJob::KilledJobError) appendLine(i18n("*** Process aborted ***")); else appendLine(i18n("*** Crashed with return code: %1 ***", QString::number(exitCode))); } qCDebug(PLUGIN_EXTERNALSCRIPT) << "Process done"; emitResult(); } void ExternalScriptJob::processError(QProcess::ProcessError error) { if (error == QProcess::FailedToStart) { setError(-1); QString errmsg = i18n("*** Could not start program '%1'. Make sure that the " "path is specified correctly ***", m_proc->program().join(QLatin1Char(' '))); appendLine(errmsg); setErrorText(errmsg); emitResult(); } qCDebug(PLUGIN_EXTERNALSCRIPT) << "Process error"; } void ExternalScriptJob::appendLine(const QString& l) { if (KDevelop::OutputModel* m = model()) { m->appendLine(l); } } KDevelop::OutputModel* ExternalScriptJob::model() { return qobject_cast(OutputJob::model()); } void ExternalScriptJob::receivedStderrLines(const QStringList& lines) { m_stderr += lines; } void ExternalScriptJob::receivedStdoutLines(const QStringList& lines) { m_stdout += lines; } diff --git a/plugins/filemanager/CMakeLists.txt b/plugins/filemanager/CMakeLists.txt index fd78112215..a188dbad3d 100644 --- a/plugins/filemanager/CMakeLists.txt +++ b/plugins/filemanager/CMakeLists.txt @@ -1,15 +1,23 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevfilemanager\") set(kdevfilemanager_PART_SRCS kdevfilemanagerplugin.cpp filemanager.cpp bookmarkhandler.cpp ) declare_qt_logging_category(kdevfilemanager_PART_SRCS TYPE PLUGIN IDENTIFIER PLUGIN_FILEMANAGER CATEGORY_BASENAME "filemanager" ) qt5_add_resources(kdevfilemanager_PART_SRCS kdevfilemanager.qrc) kdevplatform_add_plugin(kdevfilemanager JSON kdevfilemanager.json SOURCES ${kdevfilemanager_PART_SRCS}) -target_link_libraries(kdevfilemanager KF5::Bookmarks KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::TextEditor KDev::Interfaces) +target_link_libraries(kdevfilemanager + KDev::Interfaces + KDev::Sublime + KF5::Bookmarks + KF5::KIOCore + KF5::KIOFileWidgets + KF5::KIOWidgets + KF5::TextEditor +) diff --git a/plugins/filemanager/filemanager.cpp b/plugins/filemanager/filemanager.cpp index dda953cb77..5ed2d005b2 100644 --- a/plugins/filemanager/filemanager.cpp +++ b/plugins/filemanager/filemanager.cpp @@ -1,215 +1,217 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * Copyright 2006 Andreas Pakulat * * Copyright 2016 Imran Tatriev * * * * 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 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 "filemanager.h" #include #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include +#include #include "../openwith/iopenwith.h" #include "kdevfilemanagerplugin.h" #include "bookmarkhandler.h" #include "debug.h" FileManager::FileManager(KDevFileManagerPlugin *plugin, QWidget* parent) : QWidget(parent), m_plugin(plugin) { setObjectName(QStringLiteral("FileManager")); setWindowIcon(QIcon::fromTheme(QStringLiteral("folder-sync"), windowIcon())); setWindowTitle(i18n("File System")); KConfigGroup cg = KDevelop::ICore::self()->activeSession()->config()->group( "Filesystem" ); auto *l = new QVBoxLayout(this); l->setMargin(0); l->setSpacing(0); auto* model = new KFilePlacesModel( this ); urlnav = new KUrlNavigator(model, QUrl(cg.readEntry( "LastLocation", QUrl::fromLocalFile( QDir::homePath() ) )), this ); connect(urlnav, &KUrlNavigator::urlChanged, this, &FileManager::gotoUrl); l->addWidget(urlnav); dirop = new KDirOperator( urlnav->locationUrl(), this); dirop->setView( KFile::Tree ); dirop->setupMenu( KDirOperator::SortActions | KDirOperator::FileActions | KDirOperator::NavActions | KDirOperator::ViewActions ); connect(dirop, &KDirOperator::urlEntered, this, &FileManager::updateNav); connect(dirop, &KDirOperator::contextMenuAboutToShow, this, &FileManager::fillContextMenu); l->addWidget(dirop); connect( dirop, &KDirOperator::fileSelected, this, &FileManager::openFile ); setFocusProxy(dirop); // includes some actions, but not hooked into the shortcut dialog atm m_actionCollection = new KActionCollection(this); m_actionCollection->addAssociatedWidget(this); setupActions(); // Connect the bookmark handler connect(m_bookmarkHandler, &BookmarkHandler::openUrl, this, &FileManager::gotoUrl); connect(m_bookmarkHandler, &BookmarkHandler::openUrl, this, &FileManager::updateNav); } FileManager::~FileManager() { KConfigGroup cg = KDevelop::ICore::self()->activeSession()->config()->group( "Filesystem" ); cg.writeEntry( "LastLocation", urlnav->locationUrl() ); cg.sync(); } void FileManager::fillContextMenu(const KFileItem& item, QMenu* menu) { for (QAction* a : qAsConst(contextActions)) { if(menu->actions().contains(a)){ menu->removeAction(a); } } contextActions.clear(); contextActions.append(menu->addSeparator()); menu->addAction(newFileAction); contextActions.append(newFileAction); KDevelop::FileContext context(QList() << item.url()); QList extensions = KDevelop::ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, menu); KDevelop::ContextMenuExtension::populateMenu(menu, extensions); auto* tmpMenu = new QMenu(); KDevelop::ContextMenuExtension::populateMenu(tmpMenu, extensions); contextActions.append(tmpMenu->actions()); delete tmpMenu; } void FileManager::openFile(const KFileItem& file) { KDevelop::IOpenWith::openFiles(QList() << file.url()); } void FileManager::gotoUrl( const QUrl& url ) { dirop->setUrl( url, true ); } void FileManager::updateNav( const QUrl& url ) { urlnav->setLocationUrl( url ); } void FileManager::setupActions() { KActionMenu *acmBookmarks = new KActionMenu(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks"), this); acmBookmarks->setDelayed(false); m_bookmarkHandler = new BookmarkHandler(this, acmBookmarks->menu()); acmBookmarks->setShortcutContext(Qt::WidgetWithChildrenShortcut); auto* action = new QAction(this); action->setShortcutContext(Qt::WidgetWithChildrenShortcut); action->setText(i18n("Current Document Directory")); action->setIcon(QIcon::fromTheme(QStringLiteral("dirsync"))); connect(action, &QAction::triggered, this, &FileManager::syncCurrentDocumentDirectory); auto* diropActionCollection = dirop->actionCollection(); tbActions = { diropActionCollection->action(QStringLiteral("back")), diropActionCollection->action(QStringLiteral("up")), diropActionCollection->action(QStringLiteral("home")), diropActionCollection->action(QStringLiteral("forward")), diropActionCollection->action(QStringLiteral("reload")), acmBookmarks, action, diropActionCollection->action(QStringLiteral("sorting menu")), diropActionCollection->action(QStringLiteral("show hidden")), }; newFileAction = new QAction(this); newFileAction->setText(i18n("New File...")); newFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); connect(newFileAction, &QAction::triggered, this, &FileManager::createNewFile); } void FileManager::createNewFile() { QUrl destUrl = QFileDialog::getSaveFileUrl(KDevelop::ICore::self()->uiController()->activeMainWindow(), i18n("Create New File")); if (destUrl.isEmpty()) { return; } KJob* job = KIO::storedPut(QByteArray(), destUrl, -1); KJobWidgets::setWindow(job, this); connect(job, &KJob::result, this, &FileManager::fileCreated); } void FileManager::fileCreated(KJob* job) { auto transferJob = qobject_cast(job); Q_ASSERT(transferJob); if (!transferJob->error()) { KDevelop::ICore::self()->documentController()->openDocument( transferJob->url() ); } else { - KMessageBox::error(KDevelop::ICore::self()->uiController()->activeMainWindow(), i18n("Unable to create file '%1'", transferJob->url().toDisplayString(QUrl::PreferLocalFile))); + const QString messageText = i18n("Unable to create file '%1'", transferJob->url().toDisplayString(QUrl::PreferLocalFile)); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); } } void FileManager::syncCurrentDocumentDirectory() { if( KDevelop::IDocument* activeDoc = KDevelop::ICore::self()->documentController()->activeDocument() ) updateNav( activeDoc->url().adjusted(QUrl::RemoveFilename) ); } QList FileManager::toolBarActions() const { return tbActions; } KActionCollection* FileManager::actionCollection() const { return m_actionCollection; } KDirOperator* FileManager::dirOperator() const { return dirop; } KDevFileManagerPlugin* FileManager::plugin() const { return m_plugin; } #include "moc_filemanager.cpp" diff --git a/plugins/gdb/debugsession.cpp b/plugins/gdb/debugsession.cpp index 897a918920..48a8ebbe95 100644 --- a/plugins/gdb/debugsession.cpp +++ b/plugins/gdb/debugsession.cpp @@ -1,326 +1,327 @@ /* * GDB Debugger Support * * Copyright 1999-2001 John Birch * Copyright 2001 by Bernd Gehrmann * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * Copyright 2009 Niko Sams * * 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, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "debugsession.h" #include "debuglog.h" #include "debuggerplugin.h" #include "gdb.h" #include "gdbbreakpointcontroller.h" #include "gdbframestackmodel.h" #include "mi/micommand.h" #include "stty.h" #include "variablecontroller.h" #include #include #include #include #include #include +#include +#include #include #include -#include #include #include #include #include #include #include using namespace KDevMI::GDB; using namespace KDevMI::MI; using namespace KDevelop; DebugSession::DebugSession(CppDebuggerPlugin *plugin) : MIDebugSession(plugin) { m_breakpointController = new BreakpointController(this); m_variableController = new VariableController(this); m_frameStackModel = new GdbFrameStackModel(this); if (m_plugin) m_plugin->setupToolViews(); } DebugSession::~DebugSession() { if (m_plugin) m_plugin->unloadToolViews(); } void DebugSession::setAutoDisableASLR(bool enable) { m_autoDisableASLR = enable; } BreakpointController *DebugSession::breakpointController() const { return m_breakpointController; } VariableController *DebugSession::variableController() const { return m_variableController; } GdbFrameStackModel *DebugSession::frameStackModel() const { return m_frameStackModel; } GdbDebugger *DebugSession::createDebugger() const { return new GdbDebugger; } void DebugSession::initializeDebugger() { //addCommand(new GDBCommand(GDBMI::EnableTimings, "yes")); addCommand(new CliCommand(MI::GdbShow, QStringLiteral("version"), this, &DebugSession::handleVersion)); // This makes gdb pump a variable out on one line. addCommand(MI::GdbSet, QStringLiteral("width 0")); addCommand(MI::GdbSet, QStringLiteral("height 0")); addCommand(MI::SignalHandle, QStringLiteral("SIG32 pass nostop noprint")); addCommand(MI::SignalHandle, QStringLiteral("SIG41 pass nostop noprint")); addCommand(MI::SignalHandle, QStringLiteral("SIG42 pass nostop noprint")); addCommand(MI::SignalHandle, QStringLiteral("SIG43 pass nostop noprint")); addCommand(MI::EnablePrettyPrinting); addCommand(MI::GdbSet, QStringLiteral("charset UTF-8")); addCommand(MI::GdbSet, QStringLiteral("print sevenbit-strings off")); QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevgdb/printers/gdbinit")); if (!fileName.isEmpty()) { QFileInfo fileInfo(fileName); QString quotedPrintersPath = fileInfo.dir().path() .replace(QLatin1Char('\\'), QLatin1String("\\\\")) .replace(QLatin1Char('"'), QLatin1String("\\\"")); addCommand(MI::NonMI, QStringLiteral("python sys.path.insert(0, \"%0\")").arg(quotedPrintersPath)); addCommand(MI::NonMI, QLatin1String("source ") + fileName); } // GDB can't disable ASLR on CI server. addCommand(MI::GdbSet, QStringLiteral("disable-randomization %1").arg(m_autoDisableASLR ? QLatin1String("on") : QLatin1String("off"))); qCDebug(DEBUGGERGDB) << "Initialized GDB"; } void DebugSession::configInferior(ILaunchConfiguration *cfg, IExecutePlugin *iexec, const QString &) { // Read Configuration values KConfigGroup grp = cfg->config(); bool breakOnStart = grp.readEntry(KDevMI::Config::BreakOnStartEntry, false); bool displayStaticMembers = grp.readEntry(Config::StaticMembersEntry, false); bool asmDemangle = grp.readEntry(Config::DemangleNamesEntry, true); if (breakOnStart) { BreakpointModel* m = ICore::self()->debugController()->breakpointModel(); bool found = false; const auto breakpoints = m->breakpoints(); for (Breakpoint* b : breakpoints) { if (b->location() == QLatin1String("main")) { found = true; break; } } if (!found) { m->addCodeBreakpoint(QStringLiteral("main")); } } // Needed so that breakpoint widget has a chance to insert breakpoints. // FIXME: a bit hacky, as we're really not ready for new commands. setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_ready); if (displayStaticMembers) { addCommand(MI::GdbSet, QStringLiteral("print static-members on")); } else { addCommand(MI::GdbSet, QStringLiteral("print static-members off")); } if (asmDemangle) { addCommand(MI::GdbSet, QStringLiteral("print asm-demangle on")); } else { addCommand(MI::GdbSet, QStringLiteral("print asm-demangle off")); } // Set the environment variables const EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig()); QString envProfileName = iexec->environmentProfileName(cfg); if (envProfileName.isEmpty()) { qCWarning(DEBUGGERGDB) << i18n("No environment profile specified, looks like a broken " "configuration, please check run configuration '%1'. " "Using default environment profile.", cfg->name()); envProfileName = environmentProfiles.defaultProfileName(); } const auto& envvars = environmentProfiles.createEnvironment(envProfileName, {}); for (const auto& envvar : envvars) { addCommand(GdbSet, QLatin1String("environment ") + envvar); } qCDebug(DEBUGGERGDB) << "Per inferior configuration done"; } bool DebugSession::execInferior(KDevelop::ILaunchConfiguration *cfg, IExecutePlugin *, const QString &executable) { qCDebug(DEBUGGERGDB) << "Executing inferior"; KConfigGroup grp = cfg->config(); QUrl configGdbScript = grp.readEntry(Config::RemoteGdbConfigEntry, QUrl()); QUrl runShellScript = grp.readEntry(Config::RemoteGdbShellEntry, QUrl()); QUrl runGdbScript = grp.readEntry(Config::RemoteGdbRunEntry, QUrl()); // handle remote debug if (configGdbScript.isValid()) { addCommand(MI::NonMI, QLatin1String("source ") + KShell::quoteArg(configGdbScript.toLocalFile())); } // FIXME: have a check box option that controls remote debugging if (runShellScript.isValid()) { // Special for remote debug, the remote inferior is started by this shell script QByteArray tty(m_tty->getSlave().toLatin1()); QByteArray options = QByteArray(">") + tty + QByteArray(" 2>&1 <") + tty; auto *proc = new QProcess; const QStringList arguments{ QStringLiteral("-c"), KShell::quoteArg(runShellScript.toLocalFile()) + QLatin1Char(' ') + KShell::quoteArg(executable) + QString::fromLatin1(options), }; qCDebug(DEBUGGERGDB) << "starting sh" << arguments; proc->start(QStringLiteral("sh"), arguments); //PORTING TODO QProcess::DontCare); } if (runGdbScript.isValid()) { // Special for remote debug, gdb script at run is requested, to connect to remote inferior // Race notice: wait for the remote gdbserver/executable // - but that might be an issue for this script to handle... // Note: script could contain "run" or "continue" // Future: the shell script should be able to pass info (like pid) // to the gdb script... addCommand(new SentinelCommand([this, runGdbScript]() { breakpointController()->initSendBreakpoints(); breakpointController()->setDeleteDuplicateBreakpoints(true); qCDebug(DEBUGGERGDB) << "Running gdb script " << KShell::quoteArg(runGdbScript.toLocalFile()); addCommand(MI::NonMI, QLatin1String("source ") + KShell::quoteArg(runGdbScript.toLocalFile()), [this](const MI::ResultRecord&) { breakpointController()->setDeleteDuplicateBreakpoints(false); }, CmdMaybeStartsRunning); raiseEvent(connected_to_program); }, CmdMaybeStartsRunning)); } else { // normal local debugging addCommand(MI::FileExecAndSymbols, KShell::quoteArg(executable), this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); raiseEvent(connected_to_program); addCommand(new SentinelCommand([this]() { breakpointController()->initSendBreakpoints(); addCommand(MI::ExecRun, QString(), CmdMaybeStartsRunning); }, CmdMaybeStartsRunning)); } return true; } bool DebugSession::loadCoreFile(KDevelop::ILaunchConfiguration*, const QString& debugee, const QString& corefile) { addCommand(MI::FileExecAndSymbols, debugee, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); raiseEvent(connected_to_program); addCommand(NonMI, QLatin1String("core ") + corefile, this, &DebugSession::handleCoreFile, CmdHandlesError); return true; } void DebugSession::handleVersion(const QStringList& s) { qCDebug(DEBUGGERGDB) << s.first(); // minimal version is 7.0,0 QRegExp rx(QStringLiteral("([7-9]+)\\.([0-9]+)(\\.([0-9]+))?")); int idx = rx.indexIn(s.first()); if (idx == -1) { if (!qobject_cast(qApp)) { //for unittest qFatal("You need a graphical application."); } - KMessageBox::error( - qApp->activeWindow(), + const QString messageText = i18n("You need gdb 7.0.0 or higher.
" - "You are using: %1", s.first()), - i18n("gdb error")); + "You are using: %1", s.first()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); stopDebugger(); } } void DebugSession::handleFileExecAndSymbols(const ResultRecord& r) { if (r.reason == QLatin1String("error")) { - KMessageBox::error( - qApp->activeWindow(), + const QString messageText = i18n("Could not start debugger:
")+ - r[QStringLiteral("msg")].literal(), - i18n("Startup error")); + r[QStringLiteral("msg")].literal(); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); stopDebugger(); } } void DebugSession::handleCoreFile(const ResultRecord& r) { if (r.reason != QLatin1String("error")) { setDebuggerStateOn(s_programExited | s_core); } else { - KMessageBox::error( - qApp->activeWindow(), + const QString messageText = i18n("Failed to load core file" "

Debugger reported the following error:" "

%1", - r[QStringLiteral("msg")].literal()), - i18n("Startup error")); + r[QStringLiteral("msg")].literal()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); stopDebugger(); } } diff --git a/plugins/gdb/gdb.cpp b/plugins/gdb/gdb.cpp index f838f6eaaa..f542d3244a 100644 --- a/plugins/gdb/gdb.cpp +++ b/plugins/gdb/gdb.cpp @@ -1,100 +1,102 @@ /* * Low level GDB interface. * * Copyright 1999 John Birch * Copyright 2007 Vladimir Prus * Copyright 2016 Aetf * * 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, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "gdb.h" #include "dbgglobal.h" #include "debuglog.h" +#include +#include +#include + #include #include -#include #include #include #include #include using namespace KDevMI::GDB; using namespace KDevMI::MI; GdbDebugger::GdbDebugger(QObject* parent) : MIDebugger(parent) { } GdbDebugger::~GdbDebugger() { } bool GdbDebugger::start(KConfigGroup& config, const QStringList& extraArguments) { // FIXME: verify that default value leads to something sensible QUrl gdbUrl = config.readEntry(Config::GdbPathEntry, QUrl()); if (gdbUrl.isEmpty()) { m_debuggerExecutable = QStringLiteral("gdb"); } else { // FIXME: verify its' a local path. m_debuggerExecutable = gdbUrl.url(QUrl::PreferLocalFile | QUrl::StripTrailingSlash); } QStringList arguments = extraArguments; arguments << QStringLiteral("--interpreter=mi2") << QStringLiteral("-quiet"); QString fullCommand; QUrl shell = config.readEntry(Config::DebuggerShellEntry, QUrl()); if(!shell.isEmpty()) { qCDebug(DEBUGGERGDB) << "have shell" << shell; QString shell_without_args = shell.toLocalFile().split(QLatin1Char(' ')).first(); QFileInfo info(shell_without_args); /*if( info.isRelative() ) { shell_without_args = build_dir + "/" + shell_without_args; info.setFile( shell_without_args ); }*/ if(!info.exists()) { - KMessageBox::information( - qApp->activeWindow(), - i18n("Could not locate the debugging shell '%1'.", shell_without_args ), - i18n("Debugging Shell Not Found") ); + const QString messageText = i18n("Could not locate the debugging shell '%1'.", shell_without_args); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); return false; } arguments.insert(0, m_debuggerExecutable); arguments.insert(0, shell.toLocalFile()); m_process->setShellCommand(KShell::joinArgs(arguments)); } else { m_process->setProgram(m_debuggerExecutable, arguments); fullCommand = m_debuggerExecutable + QLatin1Char(' '); } fullCommand += arguments.join(QLatin1Char(' ')); m_process->start(); qCDebug(DEBUGGERGDB) << "Starting GDB with command" << fullCommand; qCDebug(DEBUGGERGDB) << "GDB process pid:" << m_process->pid(); emit userCommandOutput(fullCommand + QLatin1Char('\n')); return true; } diff --git a/plugins/heaptrack/plugin.cpp b/plugins/heaptrack/plugin.cpp index 1b08340fd5..e11800ce66 100644 --- a/plugins/heaptrack/plugin.cpp +++ b/plugins/heaptrack/plugin.cpp @@ -1,184 +1,186 @@ /* This file is part of KDevelop Copyright 2017 Anton Anikin 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plugin.h" #include "config/globalconfigpage.h" #include "debug.h" #include "job.h" #include "utils.h" #include "visualizer.h" #include #if KF5SysGuard_FOUND #include "dialogs/processselection.h" #include #endif #include #include +#include #include #include #include #include +#include #include - +// KF #include -#include #include - +// Qt #include #include #include K_PLUGIN_FACTORY_WITH_JSON(HeaptrackFactory, "kdevheaptrack.json", registerPlugin();) namespace Heaptrack { Plugin::Plugin(QObject* parent, const QVariantList&) : IPlugin(QStringLiteral("kdevheaptrack"), parent) { setXMLFile(QStringLiteral("kdevheaptrack.rc")); m_launchAction = new QAction( QIcon::fromTheme(QStringLiteral("office-chart-area")), i18n("Run Heaptrack Analysis"), this); connect(m_launchAction, &QAction::triggered, this, &Plugin::launchHeaptrack); actionCollection()->addAction(QStringLiteral("heaptrack_launch"), m_launchAction); #if KF5SysGuard_FOUND m_attachAction = new QAction( QIcon::fromTheme(QStringLiteral("office-chart-area")), i18n("Attach to Process with Heaptrack"), this); connect(m_attachAction, &QAction::triggered, this, &Plugin::attachHeaptrack); actionCollection()->addAction(QStringLiteral("heaptrack_attach"), m_attachAction); #endif } Plugin::~Plugin() { } void Plugin::launchHeaptrack() { IExecutePlugin* executePlugin = nullptr; // First we should check that our "kdevexecute" plugin is loaded. This is needed since // current plugin controller logic allows us to unload this plugin with keeping dependent // plugins like Heaptrack in "loaded" state. This seems to be wrong behaviour but now we have // to do additional checks. // TODO fix plugin controller to avoid such inconsistent states. auto pluginController = core()->pluginController(); if (auto plugin = pluginController->pluginForExtension( QStringLiteral("org.kdevelop.IExecutePlugin"), QStringLiteral("kdevexecute"))) { executePlugin = plugin->extension(); } else { auto pluginInfo = pluginController->infoForPluginId(QStringLiteral("kdevexecute")); - KMessageBox::error( - qApp->activeWindow(), - i18n("Unable to start Heaptrack analysis - \"%1\" plugin is not loaded.", pluginInfo.name())); + const QString messageText = i18n("Unable to start Heaptrack analysis - \"%1\" plugin is not loaded.", pluginInfo.name()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); return; } auto runController = KDevelop::Core::self()->runControllerInternal(); auto defaultLaunch = runController->defaultLaunch(); if (!defaultLaunch) { runController->showConfigurationDialog(); } + // TODO: catch if still no defaultLaunch if (!defaultLaunch->type()->launcherForId(QStringLiteral("nativeAppLauncher"))) { - KMessageBox::error( - qApp->activeWindow(), - i18n("Heaptrack analysis can be started only for native applications.")); + const QString messageText = i18n("Heaptrack analysis can be started only for native applications."); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); return; } auto heaptrackJob = new Job(defaultLaunch, executePlugin); connect(heaptrackJob, &Job::finished, this, &Plugin::jobFinished); QList jobList; if (KJob* depJob = executePlugin->dependencyJob(defaultLaunch)) { jobList += depJob; } jobList += heaptrackJob; auto ecJob = new KDevelop::ExecuteCompositeJob(runController, jobList); ecJob->setObjectName(heaptrackJob->statusName()); runController->registerJob(ecJob); m_launchAction->setEnabled(false); } void Plugin::attachHeaptrack() { #if KF5SysGuard_FOUND QPointer dlg = new KDevMI::ProcessSelectionDialog(activeMainWindow()); if (!dlg->exec() || !dlg->pidSelected()) { delete dlg; return; } auto heaptrackJob = new Job(dlg->pidSelected()); delete dlg; connect(heaptrackJob, &Job::finished, this, &Plugin::jobFinished); heaptrackJob->setObjectName(heaptrackJob->statusName()); core()->runController()->registerJob(heaptrackJob); m_launchAction->setEnabled(false); #endif } void Plugin::jobFinished(KJob* kjob) { auto job = static_cast(kjob); Q_ASSERT(job); if (job->status() == KDevelop::OutputExecuteJob::JobStatus::JobSucceeded) { auto visualizer = new Visualizer(job->resultsFile(), this); visualizer->start(); } else { QFile::remove(job->resultsFile()); } m_launchAction->setEnabled(true); } int Plugin::configPages() const { return 1; } KDevelop::ConfigPage* Plugin::configPage(int number, QWidget* parent) { if (number) { return nullptr; } return new GlobalConfigPage(this, parent); } } #include "plugin.moc" diff --git a/plugins/heaptrack/visualizer.cpp b/plugins/heaptrack/visualizer.cpp index ff94fd5354..602ff38d25 100644 --- a/plugins/heaptrack/visualizer.cpp +++ b/plugins/heaptrack/visualizer.cpp @@ -1,69 +1,72 @@ /* This file is part of KDevelop Copyright 2017 Anton Anikin 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "visualizer.h" #include "debug.h" #include "globalsettings.h" #include "utils.h" - +// KDevPlatform +#include +#include +#include #include - +// KF #include -#include - +// Qt #include namespace Heaptrack { Visualizer::Visualizer(const QString& resultsFile, QObject* parent) : QProcess(parent) , m_resultsFile(resultsFile) { connect(this, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { QString errorMessage; if (error == QProcess::FailedToStart) { - errorMessage = i18n("Failed to start visualizer from \"%1\".", program()) + errorMessage = i18n("Failed to start Heaptrack visualizer from \"%1\".", program()) + QLatin1String("\n\n") + i18n("Check your settings and install the visualizer if necessary."); } else { - errorMessage = i18n("Error during visualizer execution:") + errorMessage = i18n("Error during Heaptrack visualizer execution:") + QLatin1String("\n\n") + errorString(); } - KMessageBox::error(activeMainWindow(), errorMessage, i18n("Heaptrack Error")); + auto* message = new Sublime::Message(errorMessage, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); }); connect(this, QOverload::of(&QProcess::finished), this, [this]() { deleteLater(); }); setProgram(KDevelop::Path(GlobalSettings::heaptrackGuiExecutable()).toLocalFile()); setArguments({ resultsFile }); } Visualizer::~Visualizer() { QFile::remove(m_resultsFile); } } diff --git a/plugins/kdeprovider/CMakeLists.txt b/plugins/kdeprovider/CMakeLists.txt index 72458248f6..edd305804b 100644 --- a/plugins/kdeprovider/CMakeLists.txt +++ b/plugins/kdeprovider/CMakeLists.txt @@ -1,20 +1,21 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevkdeprovider\") if(BUILD_TESTING) add_subdirectory(tests) endif() set(kdevkdeprovider_PART_SRCS kdeprojectsmodel.cpp filterproxysearchline.cpp kdeproviderwidget.cpp kdeproviderplugin.cpp kdeprojectsmodel.cpp kdeprojectsreader.cpp ) kconfig_add_kcfg_files(kdevkdeprovider_PART_SRCS kdeconfig.kcfgc) ki18n_wrap_ui(kdevkdeprovider_PART_SRCS kdeconfig.ui) kdevplatform_add_plugin(kdevkdeprovider JSON kdevkdeprovider.json SOURCES ${kdevkdeprovider_PART_SRCS}) target_link_libraries(kdevkdeprovider KDev::Interfaces + KDev::Sublime KDev::Vcs ) diff --git a/plugins/lldb/debugsession.cpp b/plugins/lldb/debugsession.cpp index 228c175410..1bdbb998b6 100644 --- a/plugins/lldb/debugsession.cpp +++ b/plugins/lldb/debugsession.cpp @@ -1,496 +1,495 @@ /* * LLDB Debugger Support * Copyright 2016 Aetf * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "debugsession.h" #include "controllers/variable.h" #include "dbgglobal.h" #include "debuggerplugin.h" #include "debuglog.h" #include "lldbcommand.h" #include "mi/micommand.h" #include "stty.h" #include "stringhelpers.h" #include #include #include #include #include +#include #include +#include #include #include #include #include #include #include #include #include #include #include using namespace KDevMI::LLDB; using namespace KDevMI::MI; using namespace KDevMI; using namespace KDevelop; struct ExecRunHandler : public MICommandHandler { explicit ExecRunHandler(DebugSession *session, int maxRetry = 5) : m_session(session) , m_remainRetry(maxRetry) , m_activeCommands(1) { } void handle(const ResultRecord& r) override { --m_activeCommands; if (r.reason == QLatin1String("error")) { if (r.hasField(QStringLiteral("msg")) && r[QStringLiteral("msg")].literal().contains(QLatin1String("Invalid process during debug session"))) { // for some unknown reason, lldb-mi sometimes fails to start process if (m_remainRetry && m_session) { qCDebug(DEBUGGERLLDB) << "Retry starting"; --m_remainRetry; // resend the command again. ++m_activeCommands; m_session->addCommand(ExecRun, QString(), this, // use *this as handler, so we can track error times CmdMaybeStartsRunning | CmdHandlesError); return; } } qCDebug(DEBUGGERLLDB) << "Failed to start inferior:" << "exceeded retry times or session become invalid"; m_session->stopDebugger(); } if (m_activeCommands == 0) delete this; } bool handlesError() override { return true; } bool autoDelete() override { return false; } QPointer m_session; int m_remainRetry; int m_activeCommands; }; DebugSession::DebugSession(LldbDebuggerPlugin *plugin) : MIDebugSession(plugin) , m_formatterPath() { m_breakpointController = new BreakpointController(this); m_variableController = new VariableController(this); m_frameStackModel = new LldbFrameStackModel(this); if (m_plugin) m_plugin->setupToolViews(); connect(this, &DebugSession::stateChanged, this, &DebugSession::handleSessionStateChange); } DebugSession::~DebugSession() { if (m_plugin) m_plugin->unloadToolViews(); } BreakpointController *DebugSession::breakpointController() const { return m_breakpointController; } VariableController *DebugSession::variableController() const { return m_variableController; } LldbFrameStackModel *DebugSession::frameStackModel() const { return m_frameStackModel; } LldbDebugger *DebugSession::createDebugger() const { return new LldbDebugger; } MICommand *DebugSession::createCommand(MI::CommandType type, const QString& arguments, MI::CommandFlags flags) const { return new LldbCommand(type, arguments, flags); } MICommand *DebugSession::createUserCommand(const QString& cmd) const { if (m_hasCorrectCLIOutput) return MIDebugSession::createUserCommand(cmd); auto msg = i18n("Attempting to execute user command on unsupported LLDB version"); emit debuggerInternalOutput(msg); qCDebug(DEBUGGERLLDB) << "Attempting user command on unsupported LLDB version"; return nullptr; } void DebugSession::setFormatterPath(const QString &path) { m_formatterPath = path; } void DebugSession::initializeDebugger() { //addCommand(MI::EnableTimings, "yes"); // Check version addCommand(new CliCommand(MI::NonMI, QStringLiteral("version"), this, &DebugSession::handleVersion)); // load data formatter auto formatterPath = m_formatterPath; if (!QFileInfo(formatterPath).isFile()) { formatterPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevlldb/formatters/all.py")); } if (!formatterPath.isEmpty()) { addCommand(MI::NonMI, QLatin1String("command script import ") + KShell::quoteArg(formatterPath)); } // Treat char array as string addCommand(MI::GdbSet, QStringLiteral("print char-array-as-string on")); // set a larger term width. // TODO: set term-width to exact max column count in console view addCommand(MI::NonMI, QStringLiteral("settings set term-width 1024")); qCDebug(DEBUGGERLLDB) << "Initialized LLDB"; } void DebugSession::configInferior(ILaunchConfiguration *cfg, IExecutePlugin *iexec, const QString &executable) { // Read Configuration values KConfigGroup grp = cfg->config(); // Create target as early as possible, so we can do target specific configuration later QString filesymbols = Utils::quote(executable); bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false); if (remoteDebugging) { auto connStr = grp.readEntry(Config::LldbRemoteServerEntry, QString()); auto remoteDir = grp.readEntry(Config::LldbRemotePathEntry, QString()); auto remoteExe = QDir(remoteDir).filePath(QFileInfo(executable).fileName()); filesymbols += QLatin1String(" -r ") + Utils::quote(remoteExe); addCommand(MI::FileExecAndSymbols, filesymbols, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); addCommand(MI::TargetSelect, QLatin1String("remote ") + connStr, this, &DebugSession::handleTargetSelect, CmdHandlesError); // ensure executable is on remote end addCommand(MI::NonMI, QStringLiteral("platform mkdir -v 755 %0").arg(Utils::quote(remoteDir))); addCommand(MI::NonMI, QStringLiteral("platform put-file %0 %1") .arg(Utils::quote(executable), Utils::quote(remoteExe))); } else { addCommand(MI::FileExecAndSymbols, filesymbols, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); } raiseEvent(connected_to_program); // Set the environment variables has effect only after target created const EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig()); QString envProfileName = iexec->environmentProfileName(cfg); if (envProfileName.isEmpty()) { envProfileName = environmentProfiles.defaultProfileName(); } const auto &envVariables = environmentProfiles.variables(envProfileName); if (!envVariables.isEmpty()) { QStringList vars; vars.reserve(envVariables.size()); for (auto it = envVariables.constBegin(), ite = envVariables.constEnd(); it != ite; ++it) { vars.append(QStringLiteral("%0=%1").arg(it.key(), Utils::quote(it.value()))); } // actually using lldb command 'settings set target.env-vars' which accepts multiple values addCommand(GdbSet, QLatin1String("environment ") + vars.join(QLatin1Char(' '))); } // Break on start: can't use "-exec-run --start" because in lldb-mi // the inferior stops without any notification bool breakOnStart = grp.readEntry(KDevMI::Config::BreakOnStartEntry, false); if (breakOnStart) { BreakpointModel* m = ICore::self()->debugController()->breakpointModel(); bool found = false; const auto breakpoints = m->breakpoints(); for (Breakpoint* b : breakpoints) { if (b->location() == QLatin1String("main")) { found = true; break; } } if (!found) { m->addCodeBreakpoint(QStringLiteral("main")); } } // Needed so that breakpoint widget has a chance to insert breakpoints. // FIXME: a bit hacky, as we're really not ready for new commands. setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_ready); qCDebug(DEBUGGERLLDB) << "Per inferior configuration done"; } bool DebugSession::execInferior(ILaunchConfiguration *cfg, IExecutePlugin *, const QString &) { qCDebug(DEBUGGERLLDB) << "Executing inferior"; KConfigGroup grp = cfg->config(); // Start inferior bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false); QUrl configLldbScript = grp.readEntry(Config::LldbConfigScriptEntry, QUrl()); addCommand(new SentinelCommand([this, remoteDebugging, configLldbScript]() { // setup inferior I/O redirection if (!remoteDebugging) { // FIXME: a hacky way to emulate tty setting on linux. Not sure if this provides all needed // functionalities of a pty. Should make this conditional on other platforms. // no need to quote, settings set takes 'raw' input addCommand(MI::NonMI, QStringLiteral("settings set target.input-path %0").arg(m_tty->getSlave())); addCommand(MI::NonMI, QStringLiteral("settings set target.output-path %0").arg(m_tty->getSlave())); addCommand(MI::NonMI, QStringLiteral("settings set target.error-path %0").arg(m_tty->getSlave())); } else { // what is the expected behavior for using external terminal when doing remote debugging? } // send breakpoints already in our breakpoint model to lldb auto bc = breakpointController(); bc->initSendBreakpoints(); qCDebug(DEBUGGERLLDB) << "Turn on delete duplicate mode"; // turn on delete duplicate breakpoints model, so that breakpoints created by user command in // the script and returned as a =breakpoint-created notification won't get duplicated with the // one already in our model. // we will turn this model off once we first reach a paused state, and from that time on, // the user can create duplicated breakpoints using normal command. bc->setDeleteDuplicateBreakpoints(true); // run custom config script right before we starting the inferior, // so the user has the freedom to change everything. if (configLldbScript.isValid()) { addCommand(MI::NonMI, QLatin1String("command source -s 0 ") + KShell::quoteArg(configLldbScript.toLocalFile())); } addCommand(MI::ExecRun, QString(), new ExecRunHandler(this), CmdMaybeStartsRunning | CmdHandlesError); }, CmdMaybeStartsRunning)); return true; } bool DebugSession::loadCoreFile(ILaunchConfiguration *, const QString &debugee, const QString &corefile) { addCommand(MI::FileExecAndSymbols, debugee, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); raiseEvent(connected_to_program); addCommand(new CliCommand(NonMI, QLatin1String("target create -c ") + Utils::quote(corefile), this, &DebugSession::handleCoreFile, CmdHandlesError)); return true; } void DebugSession::interruptDebugger() { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; addCommand(ExecInterrupt, QString(), CmdInterrupt); return; } void DebugSession::ensureDebuggerListening() { // lldb always uses async mode and prompt is always available. // no need to interrupt setDebuggerStateOff(s_dbgNotListening); // NOTE: there is actually a bug in lldb-mi that, when receiving SIGINT, // it would print '^done', which doesn't corresponds to any previous command. // This confuses our command queue. } void DebugSession::handleFileExecAndSymbols(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { - KMessageBox::error( - qApp->activeWindow(), - i18n("Could not start debugger:
")+ - r[QStringLiteral("msg")].literal(), - i18n("Startup error")); + const QString messageText = i18n("Could not start debugger:
")+ + r[QStringLiteral("msg")].literal(); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); stopDebugger(); } } void DebugSession::handleTargetSelect(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { - KMessageBox::error(qApp->activeWindow(), - i18n("Error connecting to remote target:
")+ - r[QStringLiteral("msg")].literal(), - i18n("Startup error")); + const QString messageText = i18n("Error connecting to remote target:
")+ + r[QStringLiteral("msg")].literal(); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); stopDebugger(); } } void DebugSession::handleCoreFile(const QStringList &s) { qCDebug(DEBUGGERLLDB) << s; for (const auto &line : s) { if (line.startsWith(QLatin1String("error:"))) { - KMessageBox::error( - qApp->activeWindow(), - i18n("Failed to load core file" + const QString messageText = i18n("Failed to load core file" "

Debugger reported the following error:" "

%1", - s.join(QLatin1Char('\n'))), - i18n("Startup error")); + s.join(QLatin1Char('\n'))); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); stopDebugger(); return; } } // There's no "thread-group-started" notification from lldb-mi, so pretend we have received one. // see MIDebugSession::processNotification(const MI::AsyncRecord & async) setDebuggerStateOff(s_appNotStarted | s_programExited); setDebuggerStateOn(s_programExited | s_core); } void DebugSession::handleVersion(const QStringList& s) { m_hasCorrectCLIOutput = !s.isEmpty(); if (!m_hasCorrectCLIOutput) { // No output from 'version' command. It's likely that // the lldb used is not patched for the CLI output if (!qobject_cast(qApp)) { //for unittest qFatal("You need a graphical application."); } auto ans = KMessageBox::warningYesNo( qApp->activeWindow(), i18n("Your lldb-mi version is unsupported, as it lacks an essential patch.
" "See https://llvm.org/bugs/show_bug.cgi?id=28026 for more information.
" "Debugger console will be disabled to prevent crash.
" "Do you want to continue?"), i18n("LLDB Version Unsupported"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("unsupported-lldb-debugger")); if (ans == KMessageBox::ButtonCode::No) { programFinished(QStringLiteral("Stopped because of unsupported LLDB version")); stopDebugger(); } return; } qCDebug(DEBUGGERLLDB) << s.first(); // minimal version is 3.8.1 #ifdef Q_OS_OSX QRegularExpression rx(QStringLiteral("^lldb-(\\d+).(\\d+).(\\d+)\\b"), QRegularExpression::MultilineOption); // lldb 3.8.1 reports version 350.99.0 on OS X const int min_ver[] = {350, 99, 0}; #else QRegularExpression rx(QStringLiteral("^lldb version (\\d+).(\\d+).(\\d+)\\b"), QRegularExpression::MultilineOption); const int min_ver[] = {3, 8, 1}; #endif auto match = rx.match(s.first()); int version[] = {0, 0, 0}; if (match.hasMatch()) { for (int i = 0; i != 3; ++i) { version[i] = match.capturedRef(i+1).toInt(); } } bool ok = true; for (int i = 0; i < 3; ++i) { if (version[i] < min_ver[i]) { ok = false; break; } else if (version[i] > min_ver[i]) { ok = true; break; } } if (!ok) { if (!qobject_cast(qApp)) { //for unittest qFatal("You need a graphical application."); } - KMessageBox::error( - qApp->activeWindow(), - i18n("You need lldb-mi from LLDB 3.8.1 or higher.
" - "You are using: %1", s.first()), - i18n("LLDB Error")); + const QString messageText = i18n("You need lldb-mi from LLDB 3.8.1 or higher.
" + "You are using: %1", s.first()); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); stopDebugger(); } } void DebugSession::updateAllVariables() { // FIXME: this is only a workaround for lldb-mi doesn't provide -var-update changelist // for variables that have a python synthetic provider. Remove this after this is fixed // in the upstream. // re-fetch all toplevel variables, as -var-update doesn't work with data formatter // we have to pick out top level variables first, as refetching will delete child // variables. QList toplevels; for (auto* variable : qAsConst(m_allVariables)) { auto *var = qobject_cast(variable); if (var->topLevel()) { toplevels << var; } } for (auto var : qAsConst(toplevels)) { var->refetch(); } } void DebugSession::handleSessionStateChange(IDebugSession::DebuggerState state) { if (state == IDebugSession::PausedState) { // session is paused, the user can input any commands now. // Turn off delete duplicate breakpoints mode, as the user // may intentionally want to do this. qCDebug(DEBUGGERLLDB) << "Turn off delete duplicate mode"; breakpointController()->setDeleteDuplicateBreakpoints(false); } } diff --git a/plugins/patchreview/patchhighlighter.cpp b/plugins/patchreview/patchhighlighter.cpp index 5037bef9dc..5d309a8f4d 100644 --- a/plugins/patchreview/patchhighlighter.cpp +++ b/plugins/patchreview/patchhighlighter.cpp @@ -1,697 +1,699 @@ /*************************************************************************** Copyright 2006 David Nolden ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "patchhighlighter.h" #include #include #include "patchreview.h" #include "debug.h" #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include using namespace KDevelop; namespace { QPointer currentTooltip; KTextEditor::MovingRange* currentTooltipMark; QSize sizeHintForHtml( const QString& html, QSize maxSize ) { QTextDocument doc; doc.setHtml( html ); QSize ret; if( doc.idealWidth() > maxSize.width() ) { doc.setPageSize( QSize( maxSize.width(), 30 ) ); ret.setWidth( maxSize.width() ); }else{ ret.setWidth( doc.idealWidth() ); } ret.setHeight( doc.size().height() ); if( ret.height() > maxSize.height() ) ret.setHeight( maxSize.height() ); return ret; } } const unsigned int PatchHighlighter::m_allmarks = KTextEditor::MarkInterface::markType22 | KTextEditor::MarkInterface::markType23 | KTextEditor::MarkInterface::markType24 | KTextEditor::MarkInterface::markType25 | KTextEditor::MarkInterface::markType26 | KTextEditor::MarkInterface::markType27; void PatchHighlighter::showToolTipForMark(const QPoint& pos, KTextEditor::MovingRange* markRange) { if( currentTooltipMark == markRange && currentTooltip ) return; delete currentTooltip; //Got the difference Diff2::Difference* diff = m_ranges[markRange]; QString html; #if 0 if( diff->hasConflict() ) html += i18n( "Conflict
" ); #endif Diff2::DifferenceStringList lines; html += QLatin1String(""); if( diff->applied() ) { if( !m_plugin->patch()->isAlreadyApplied() ) html += i18n( "Applied.
" ); if( isInsertion( diff ) ) { html += i18n( "Insertion
" ); } else { if( isRemoval( diff ) ) html += i18n( "Removal
" ); html += i18n( "Previous:
" ); lines = diff->sourceLines(); } } else { if( m_plugin->patch()->isAlreadyApplied() ) html += i18n( "Reverted.
" ); if( isRemoval( diff ) ) { html += i18n( "Removal
" ); } else { if( isInsertion( diff ) ) html += i18n( "Insertion
" ); html += i18n( "Alternative:
" ); lines = diff->destinationLines(); } } html += QLatin1String("
"); for (auto* line : qAsConst(lines)) { uint currentPos = 0; const QString& string = line->string(); const Diff2::MarkerList& markers = line->markerList(); for (auto* marker : markers) { const QString spanText = string.mid( currentPos, marker->offset() - currentPos ).toHtmlEscaped(); if (marker->type() == Diff2::Marker::End && (currentPos != 0 || marker->offset() != static_cast( string.size()))) { html += QLatin1String("") + spanText + QLatin1String(""); }else{ html += spanText; } currentPos = marker->offset(); } html += string.mid(currentPos, string.length()-currentPos).toHtmlEscaped() + QLatin1String("
"); } auto browser = new QTextBrowser; browser->setPalette( QApplication::palette() ); browser->setHtml( html ); int maxHeight = 500; browser->setMinimumSize( sizeHintForHtml( html, QSize( ( ICore::self()->uiController()->activeMainWindow()->width()*2 )/3, maxHeight ) ) ); browser->setMaximumSize( browser->minimumSize() + QSize( 10, 10 ) ); if( browser->minimumHeight() != maxHeight ) browser->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); auto* layout = new QVBoxLayout; layout->setMargin( 0 ); layout->addWidget( browser ); KDevelop::ActiveToolTip* tooltip = new KDevelop::ActiveToolTip( ICore::self()->uiController()->activeMainWindow(), pos + QPoint( 5, -browser->sizeHint().height() - 30 ) ); tooltip->setLayout( layout ); tooltip->resize( tooltip->sizeHint() + QSize( 10, 10 ) ); tooltip->move( pos - QPoint( 0, 20 + tooltip->height() ) ); tooltip->setHandleRect( QRect( pos - QPoint( 15, 15 ), pos + QPoint( 15, 15 ) ) ); currentTooltip = tooltip; currentTooltipMark = markRange; ActiveToolTip::showToolTip( tooltip ); } void PatchHighlighter::markClicked( KTextEditor::Document* doc, const KTextEditor::Mark& mark, bool& handled ) { if( handled || !(mark.type & m_allmarks) ) return; auto range_diff = rangeForMark(mark); m_applying = true; if (range_diff.first) { handled = true; KTextEditor::MovingRange *&range = range_diff.first; Diff2::Difference *&diff = range_diff.second; QString currentText = doc->text( range->toRange() ); removeLineMarker( range ); QString sourceText; QString targetText; for( int a = 0; a < diff->sourceLineCount(); ++a ) { sourceText += diff->sourceLineAt( a )->string(); if (!sourceText.endsWith(QLatin1Char('\n'))) sourceText += QLatin1Char('\n'); } for( int a = 0; a < diff->destinationLineCount(); ++a ) { targetText += diff->destinationLineAt( a )->string(); if (!targetText.endsWith(QLatin1Char('\n'))) targetText += QLatin1Char('\n'); } bool applied = diff->applied(); QString &replace(applied ? targetText : sourceText); QString &replaceWith(applied ? sourceText : targetText); if( currentText.simplified() != replace.simplified() ) { - KMessageBox::error( ICore::self()->uiController()->activeMainWindow(), i18n( "Could not apply the change: Text should be \"%1\", but is \"%2\".", replace, currentText ) ); + const QString messageText = i18n("Could not apply the change: Text should be \"%1\", but is \"%2\".", replace, currentText); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); m_applying = false; return; } diff->apply(!applied); KTextEditor::Cursor start = range->start().toCursor(); range->document()->replaceText( range->toRange(), replaceWith ); const uint replaceWithLines = replaceWith.count(QLatin1Char('\n')); KTextEditor::Range newRange( start, KTextEditor::Cursor(start.line() + replaceWithLines, start.column()) ); range->setRange( newRange ); addLineMarker( range, diff ); { // After applying the change, show the tooltip again, mainly to update an old tooltip delete currentTooltip; currentTooltip = nullptr; bool h = false; markToolTipRequested( doc, mark, QCursor::pos(), h ); } } m_applying = false; } QPair PatchHighlighter::rangeForMark( const KTextEditor::Mark &mark ) { if (!m_applying) { for( QMap::const_iterator it = m_ranges.constBegin(); it != m_ranges.constEnd(); ++it ) { if (it.value() && it.key()->start().line() <= mark.line && mark.line <= it.key()->end().line()) { return qMakePair(it.key(), it.value()); } } } return qMakePair(nullptr, nullptr); } void PatchHighlighter::markToolTipRequested( KTextEditor::Document*, const KTextEditor::Mark& mark, QPoint pos, bool& handled ) { if( handled ) return; if( mark.type & m_allmarks ) { //There is a mark in this line. Show the old text. auto range = rangeForMark(mark); if( range.first ) { showToolTipForMark( pos, range.first ); handled = true; } } } bool PatchHighlighter::isInsertion( Diff2::Difference* diff ) { return diff->sourceLineCount() == 0; } bool PatchHighlighter::isRemoval( Diff2::Difference* diff ) { return diff->destinationLineCount() == 0; } void PatchHighlighter::performContentChange( KTextEditor::Document* doc, const QStringList& oldLines, const QStringList& newLines, int editLineNumber ) { QPair, QList > diffChange = m_model->linesChanged( oldLines, newLines, editLineNumber ); const QList& inserted = diffChange.first; const QList& removed = diffChange.second; for (Diff2::Difference* d : removed) { const auto sourceLines = d->sourceLines(); for (Diff2::DifferenceString* s : sourceLines) qCDebug(PLUGIN_PATCHREVIEW) << "removed source" << s->string(); const auto destinationLines = d->destinationLines(); for (Diff2::DifferenceString* s : destinationLines) qCDebug(PLUGIN_PATCHREVIEW) << "removed destination" << s->string(); } for (Diff2::Difference* d : inserted) { const auto sourceLines = d->sourceLines(); for (Diff2::DifferenceString* s : sourceLines) qCDebug(PLUGIN_PATCHREVIEW) << "inserted source" << s->string(); const auto destinationLines = d->destinationLines(); for (Diff2::DifferenceString* s : destinationLines) qCDebug(PLUGIN_PATCHREVIEW) << "inserted destination" << s->string(); } // Remove all ranges that are in the same line (the line markers) for (auto it = m_ranges.begin(); it != m_ranges.end();) { if (removed.contains(it.value())) { KTextEditor::MovingRange* r = it.key(); removeLineMarker(r); // is altering m_ranges it = m_ranges.erase(it); delete r; } else { ++it; } } qDeleteAll(removed); auto* moving = qobject_cast(doc); if ( !moving ) return; for (Diff2::Difference* diff : inserted) { int lineStart = diff->destinationLineNumber(); if ( lineStart > 0 ) { --lineStart; } int lineEnd = diff->destinationLineEnd(); if ( lineEnd > 0 ) { --lineEnd; } KTextEditor::Range newRange( lineStart, 0, lineEnd, 0 ); KTextEditor::MovingRange * r = moving->newMovingRange( newRange ); m_ranges[r] = diff; addLineMarker( r, diff ); } } void PatchHighlighter::textRemoved( KTextEditor::Document* doc, const KTextEditor::Range& range, const QString& oldText ) { if ( m_applying ) { // Do not interfere with patch application return; } qCDebug(PLUGIN_PATCHREVIEW) << "removal range" << range; qCDebug(PLUGIN_PATCHREVIEW) << "removed text" << oldText; KTextEditor::Cursor cursor = range.start(); int startLine = cursor.line(); QStringList removedLines; QStringList remainingLines; if (startLine > 0) { QString above = doc->line(--startLine); removedLines << above; remainingLines << above; } const QString changed = doc->line(cursor.line()) + QLatin1Char('\n'); removedLines << changed.midRef(0, cursor.column()) + oldText + changed.midRef(cursor.column()); remainingLines << changed; if (doc->documentRange().end().line() > cursor.line()) { QString below = doc->line(cursor.line() + 1); removedLines << below; remainingLines << below; } performContentChange(doc, removedLines, remainingLines, startLine + 1); } void PatchHighlighter::newlineRemoved(KTextEditor::Document* doc, int line) { if ( m_applying ) { // Do not interfere with patch application return; } qCDebug(PLUGIN_PATCHREVIEW) << "remove newline" << line; KTextEditor::Cursor cursor = m_doc->cursorPosition(); int startLine = line - 1; QStringList removedLines; QStringList remainingLines; if (startLine > 0) { QString above = doc->line(--startLine); removedLines << above; remainingLines << above; } QString changed = doc->line(line - 1); if (cursor.line() == line - 1) { removedLines << changed.mid(0, cursor.column()); removedLines << changed.mid(cursor.column()); } else { removedLines << changed; removedLines << QString(); } remainingLines << changed; if (doc->documentRange().end().line() >= line) { QString below = doc->line(line); removedLines << below; remainingLines << below; } performContentChange(doc, removedLines, remainingLines, startLine + 1); } void PatchHighlighter::documentReloaded(KTextEditor::Document* doc) { qCDebug(PLUGIN_PATCHREVIEW) << "re-doing"; //The document was loaded / reloaded if ( !m_model->differences() ) return; auto* moving = qobject_cast(doc); if ( !moving ) return; auto* markIface = qobject_cast(doc); if( !markIface ) return; clear(); #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,50,0) constexpr int markPixmapSize = 32; #else constexpr int markPixmapSize = 16; #endif KColorScheme scheme( QPalette::Active ); QImage tintedInsertion = QIcon::fromTheme(QStringLiteral("insert-text")).pixmap(markPixmapSize, markPixmapSize).toImage(); KIconEffect::colorize( tintedInsertion, scheme.foreground( KColorScheme::NegativeText ).color(), 1.0 ); QImage tintedRemoval = QIcon::fromTheme(QStringLiteral("edit-delete")).pixmap(markPixmapSize, markPixmapSize).toImage(); KIconEffect::colorize( tintedRemoval, scheme.foreground( KColorScheme::NegativeText ).color(), 1.0 ); QImage tintedChange = QIcon::fromTheme(QStringLiteral("text-field")).pixmap(markPixmapSize, markPixmapSize).toImage(); KIconEffect::colorize( tintedChange, scheme.foreground( KColorScheme::NegativeText ).color(), 1.0 ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType22, i18n( "Insertion" ) ); markIface->setMarkPixmap( KTextEditor::MarkInterface::markType22, QPixmap::fromImage( tintedInsertion ) ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType23, i18n( "Removal" ) ); markIface->setMarkPixmap( KTextEditor::MarkInterface::markType23, QPixmap::fromImage( tintedRemoval ) ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType24, i18n( "Change" ) ); markIface->setMarkPixmap( KTextEditor::MarkInterface::markType24, QPixmap::fromImage( tintedChange ) ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType25, i18n( "Insertion" ) ); markIface->setMarkPixmap(KTextEditor::MarkInterface::markType25, QIcon::fromTheme(QStringLiteral("insert-text")).pixmap(markPixmapSize, markPixmapSize)); markIface->setMarkDescription( KTextEditor::MarkInterface::markType26, i18n( "Removal" ) ); markIface->setMarkPixmap(KTextEditor::MarkInterface::markType26, QIcon::fromTheme(QStringLiteral("edit-delete")).pixmap(markPixmapSize, markPixmapSize)); markIface->setMarkDescription( KTextEditor::MarkInterface::markType27, i18n( "Change" ) ); markIface->setMarkPixmap(KTextEditor::MarkInterface::markType27, QIcon::fromTheme(QStringLiteral("text-field")).pixmap(markPixmapSize, markPixmapSize)); for (Diff2::Difference* diff : qAsConst(*m_model->differences())) { int line, lineCount; Diff2::DifferenceStringList lines; if( diff->applied() ) { line = diff->destinationLineNumber(); lineCount = diff->destinationLineCount(); lines = diff->destinationLines(); } else { line = diff->sourceLineNumber(); lineCount = diff->sourceLineCount(); lines = diff->sourceLines(); } if ( line > 0 ) line -= 1; KTextEditor::Cursor c( line, 0 ); KTextEditor::Cursor endC( line + lineCount, 0 ); if ( doc->lines() <= c.line() ) c.setLine( doc->lines() - 1 ); if ( doc->lines() <= endC.line() ) endC.setLine( doc->lines() ); if ( endC.isValid() && c.isValid() ) { KTextEditor::MovingRange * r = moving->newMovingRange( KTextEditor::Range( c, endC ) ); m_ranges[r] = diff; addLineMarker( r, diff ); } } } void PatchHighlighter::textInserted(KTextEditor::Document* doc, const KTextEditor::Cursor& cursor, const QString& text) { if ( m_applying ) { // Do not interfere with patch application return; } int startLine = cursor.line(); int endColumn = cursor.column() + text.length(); qCDebug(PLUGIN_PATCHREVIEW) << "insertion range" << KTextEditor::Range(cursor, KTextEditor::Cursor(startLine, endColumn)); qCDebug(PLUGIN_PATCHREVIEW) << "inserted text" << text; QStringList removedLines; QStringList insertedLines; if (startLine > 0) { const QString above = doc->line(--startLine) + QLatin1Char('\n'); removedLines << above; insertedLines << above; } const QString changed = doc->line(cursor.line()) + QLatin1Char('\n'); removedLines << changed.midRef(0, cursor.column()) + changed.midRef(endColumn); insertedLines << changed; if (doc->documentRange().end().line() > cursor.line()) { const QString below = doc->line(cursor.line() + 1) + QLatin1Char('\n'); removedLines << below; insertedLines << below; } performContentChange(doc, removedLines, insertedLines, startLine + 1); } void PatchHighlighter::newlineInserted(KTextEditor::Document* doc, const KTextEditor::Cursor& cursor) { if ( m_applying ) { // Do not interfere with patch application return; } qCDebug(PLUGIN_PATCHREVIEW) << "newline range" << KTextEditor::Range(cursor, KTextEditor::Cursor(cursor.line() + 1, 0)); int startLine = cursor.line(); QStringList removedLines; QStringList insertedLines; if (startLine > 0) { const QString above = doc->line(--startLine) + QLatin1Char('\n'); removedLines << above; insertedLines << above; } insertedLines << QStringLiteral("\n"); if (doc->documentRange().end().line() > cursor.line()) { const QString below = doc->line(cursor.line() + 1) + QLatin1Char('\n'); removedLines << below; insertedLines << below; } performContentChange(doc, removedLines, insertedLines, startLine + 1); } PatchHighlighter::PatchHighlighter( Diff2::DiffModel* model, IDocument* kdoc, PatchReviewPlugin* plugin, bool updatePatchFromEdits ) : m_doc( kdoc ), m_plugin( plugin ), m_model( model ), m_applying( false ) { KTextEditor::Document* doc = kdoc->textDocument(); // connect( kdoc, SIGNAL(destroyed(QObject*)), this, SLOT(documentDestroyed()) ); if (updatePatchFromEdits) { connect(doc, &KTextEditor::Document::textInserted, this, &PatchHighlighter::textInserted); connect(doc, &KTextEditor::Document::lineWrapped, this, &PatchHighlighter::newlineInserted); connect(doc, &KTextEditor::Document::textRemoved, this, &PatchHighlighter::textRemoved); connect(doc, &KTextEditor::Document::lineUnwrapped, this, &PatchHighlighter::newlineRemoved); } connect(doc, &KTextEditor::Document::reloaded, this, &PatchHighlighter::documentReloaded); connect(doc, &KTextEditor::Document::destroyed, this, &PatchHighlighter::documentDestroyed); if ( doc->lines() == 0 ) return; if (qobject_cast(doc)) { //can't use new signal/slot syntax here, MarkInterface is not a QObject connect(doc, SIGNAL(markToolTipRequested(KTextEditor::Document*,KTextEditor::Mark,QPoint,bool&)), this, SLOT(markToolTipRequested(KTextEditor::Document*,KTextEditor::Mark,QPoint,bool&))); connect(doc, SIGNAL(markClicked(KTextEditor::Document*,KTextEditor::Mark,bool&)), this, SLOT(markClicked(KTextEditor::Document*,KTextEditor::Mark,bool&))); } if (qobject_cast(doc)) { //can't use new signal/slot syntax here, MovingInterface is not a QObject connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*))); connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*))); } documentReloaded(doc); } void PatchHighlighter::removeLineMarker( KTextEditor::MovingRange* range ) { auto* moving = qobject_cast(range->document()); if ( !moving ) return; auto* markIface = qobject_cast(range->document()); if( !markIface ) return; for (int line = range->start().line(); line <= range->end().line(); ++line) { markIface->removeMark(line, m_allmarks); } // Remove all ranges that are in the same line (the line markers) for (auto it = m_ranges.begin(); it != m_ranges.end();) { if (it.key() != range && range->overlaps(it.key()->toRange())) { delete it.key(); it = m_ranges.erase(it); } else { ++it; } } } void PatchHighlighter::addLineMarker( KTextEditor::MovingRange* range, Diff2::Difference* diff ) { auto* moving = qobject_cast(range->document()); if ( !moving ) return; auto* markIface = qobject_cast(range->document()); if( !markIface ) return; KTextEditor::Attribute::Ptr t( new KTextEditor::Attribute() ); bool isOriginalState = diff->applied() == m_plugin->patch()->isAlreadyApplied(); if( isOriginalState ) { t->setProperty( QTextFormat::BackgroundBrush, QBrush( ColorCache::self()->blendBackground( QColor( 0, 255, 255 ), 20 ) ) ); }else{ t->setProperty( QTextFormat::BackgroundBrush, QBrush( ColorCache::self()->blendBackground( QColor( 255, 0, 255 ), 20 ) ) ); } range->setAttribute( t ); range->setZDepth( -500 ); KTextEditor::MarkInterface::MarkTypes mark; if( isOriginalState ) { mark = KTextEditor::MarkInterface::markType27; if( isInsertion( diff ) ) mark = KTextEditor::MarkInterface::markType25; if( isRemoval( diff ) ) mark = KTextEditor::MarkInterface::markType26; }else{ mark = KTextEditor::MarkInterface::markType24; if( isInsertion( diff ) ) mark = KTextEditor::MarkInterface::markType22; if( isRemoval( diff ) ) mark = KTextEditor::MarkInterface::markType23; } markIface->addMark( range->start().line(), mark ); Diff2::DifferenceStringList lines; if( diff->applied() ) lines = diff->destinationLines(); else lines = diff->sourceLines(); for( int a = 0; a < lines.size(); ++a ) { Diff2::DifferenceString* line = lines[a]; int currentPos = 0; const uint lineLength = static_cast(line->string().size()); const Diff2::MarkerList& markers = line->markerList(); for (auto* marker : markers) { if (marker->type() == Diff2::Marker::End) { if (currentPos != 0 || marker->offset() != lineLength) { KTextEditor::MovingRange* r2 = moving->newMovingRange( KTextEditor::Range( KTextEditor::Cursor( a + range->start().line(), currentPos ), KTextEditor::Cursor( a + range->start().line(), marker->offset() ) ) ); m_ranges[r2] = nullptr; KTextEditor::Attribute::Ptr t( new KTextEditor::Attribute() ); t->setProperty( QTextFormat::BackgroundBrush, QBrush( ColorCache::self()->blendBackground( QColor( 255, 0, 0 ), 70 ) ) ); r2->setAttribute( t ); r2->setZDepth( -600 ); } } currentPos = marker->offset(); } } } void PatchHighlighter::clear() { if( m_ranges.empty() ) return; auto* moving = qobject_cast(m_doc->textDocument()); if ( !moving ) return; auto* markIface = qobject_cast(m_doc->textDocument()); if( !markIface ) return; const auto lines = markIface->marks().keys(); for (int line : lines) { markIface->removeMark( line, m_allmarks ); } // Diff is taking care of its own objects (except removed ones) qDeleteAll( m_ranges.keys() ); m_ranges.clear(); } PatchHighlighter::~PatchHighlighter() { clear(); } IDocument* PatchHighlighter::doc() { return m_doc; } void PatchHighlighter::documentDestroyed() { qCDebug(PLUGIN_PATCHREVIEW) << "document destroyed"; m_ranges.clear(); } void PatchHighlighter::aboutToDeleteMovingInterfaceContent( KTextEditor::Document* ) { qCDebug(PLUGIN_PATCHREVIEW) << "about to delete"; clear(); } QList< KTextEditor::MovingRange* > PatchHighlighter::ranges() const { return m_ranges.keys(); } diff --git a/plugins/patchreview/patchreview.cpp b/plugins/patchreview/patchreview.cpp index 84db6eb5ce..c3242ffe05 100644 --- a/plugins/patchreview/patchreview.cpp +++ b/plugins/patchreview/patchreview.cpp @@ -1,634 +1,637 @@ /*************************************************************************** Copyright 2006-2009 David Nolden ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "patchreview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include ///Whether arbitrary exceptions that occurred while diff-parsing within the library should be caught #define CATCHLIBDIFF /* Exclude this file from doublequote_chars check as krazy doesn't understand std::string*/ //krazy:excludeall=doublequote_chars #include #include #include #include #include #include #include "patchhighlighter.h" #include "patchreviewtoolview.h" #include "localpatchsource.h" #include "debug.h" using namespace KDevelop; namespace { // Maximum number of files to open directly within a tab when the review is started const int maximumFilesToOpenDirectly = 15; } Q_DECLARE_METATYPE( const Diff2::DiffModel* ) void PatchReviewPlugin::seekHunk( bool forwards, const QUrl& fileName ) { try { qCDebug(PLUGIN_PATCHREVIEW) << forwards << fileName << fileName.isEmpty(); if ( !m_modelList ) throw "no model"; for ( int a = 0; a < m_modelList->modelCount(); ++a ) { const Diff2::DiffModel* model = m_modelList->modelAt( a ); if ( !model || !model->differences() ) continue; QUrl file = urlForFileModel( model ); if ( !fileName.isEmpty() && fileName != file ) continue; IDocument* doc = ICore::self()->documentController()->documentForUrl( file ); if ( doc && m_highlighters.contains( doc->url() ) && m_highlighters[doc->url()] ) { if ( doc->textDocument() ) { const QList ranges = m_highlighters[doc->url()]->ranges(); KTextEditor::View * v = doc->activeTextView(); if ( v ) { int bestLine = -1; KTextEditor::Cursor c = v->cursorPosition(); for (auto* range : ranges) { const int line = range->start().line(); if ( forwards ) { if ( line > c.line() && ( bestLine == -1 || line < bestLine ) ) bestLine = line; } else { if ( line < c.line() && ( bestLine == -1 || line > bestLine ) ) bestLine = line; } } if ( bestLine != -1 ) { v->setCursorPosition( KTextEditor::Cursor( bestLine, 0 ) ); return; } else if(fileName.isEmpty()) { int next = qBound(0, forwards ? a+1 : a-1, m_modelList->modelCount()-1); if (next < maximumFilesToOpenDirectly) { ICore::self()->documentController()->openDocument(urlForFileModel(m_modelList->modelAt(next))); } } } } } } } catch ( const QString & str ) { qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str; } catch ( const char * str ) { qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str; } qCDebug(PLUGIN_PATCHREVIEW) << "no matching hunk found"; } void PatchReviewPlugin::addHighlighting( const QUrl& highlightFile, IDocument* document ) { try { if ( !modelList() ) throw "no model"; for ( int a = 0; a < modelList()->modelCount(); ++a ) { Diff2::DiffModel* model = modelList()->modelAt( a ); if ( !model ) continue; QUrl file = urlForFileModel( model ); if ( file != highlightFile ) continue; qCDebug(PLUGIN_PATCHREVIEW) << "highlighting" << file.toDisplayString(); IDocument* doc = document; if( !doc ) doc = ICore::self()->documentController()->documentForUrl( file ); qCDebug(PLUGIN_PATCHREVIEW) << "highlighting file" << file << "with doc" << doc; if ( !doc || !doc->textDocument() ) continue; removeHighlighting( file ); m_highlighters[file] = new PatchHighlighter(model, doc, this, (qobject_cast(m_patch.data()) == nullptr)); } } catch ( const QString & str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } catch ( const char * str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } } void PatchReviewPlugin::highlightPatch() { try { if ( !modelList() ) throw "no model"; for ( int a = 0; a < modelList()->modelCount(); ++a ) { const Diff2::DiffModel* model = modelList()->modelAt( a ); if ( !model ) continue; QUrl file = urlForFileModel( model ); addHighlighting( file ); } } catch ( const QString & str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } catch ( const char * str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } } void PatchReviewPlugin::removeHighlighting( const QUrl& file ) { if ( file.isEmpty() ) { ///Remove all highlighting qDeleteAll( m_highlighters ); m_highlighters.clear(); } else { HighlightMap::iterator it = m_highlighters.find( file ); if ( it != m_highlighters.end() ) { delete *it; m_highlighters.erase( it ); } } } void PatchReviewPlugin::notifyPatchChanged() { if (m_patch) { qCDebug(PLUGIN_PATCHREVIEW) << "notifying patch change: " << m_patch->file(); m_updateKompareTimer->start(); } else { m_updateKompareTimer->stop(); } } void PatchReviewPlugin::forceUpdate() { if( m_patch ) { // don't trigger an update if we know the plugin cannot update itself auto* vcsPatch = qobject_cast(m_patch.data()); if (!vcsPatch || vcsPatch->m_updater) { m_patch->update(); notifyPatchChanged(); } } } void PatchReviewPlugin::updateKompareModel() { if ( !m_patch ) { ///TODO: this method should be cleaned up, it can be called by the timer and /// e.g. https://bugs.kde.org/show_bug.cgi?id=267187 shows how it could /// lead to asserts before... return; } qCDebug(PLUGIN_PATCHREVIEW) << "updating model"; removeHighlighting(); m_modelList.reset( nullptr ); m_depth = 0; delete m_diffSettings; { IDocument* patchDoc = ICore::self()->documentController()->documentForUrl( m_patch->file() ); if( patchDoc ) patchDoc->reload(); } QString patchFile; if( m_patch->file().isLocalFile() ) patchFile = m_patch->file().toLocalFile(); else if( m_patch->file().isValid() && !m_patch->file().isEmpty() ) { patchFile = QStandardPaths::writableLocation(QStandardPaths::TempLocation); bool ret = KIO::copy(m_patch->file(), QUrl::fromLocalFile(patchFile), KIO::HideProgressInfo)->exec(); if( !ret ) { qCWarning(PLUGIN_PATCHREVIEW) << "Problem while downloading: " << m_patch->file() << "to" << patchFile; patchFile.clear(); } } if (!patchFile.isEmpty()) //only try to construct the model if we have a patch to load try { m_diffSettings = new DiffSettings( nullptr ); m_kompareInfo.reset( new Kompare::Info() ); m_kompareInfo->localDestination = patchFile; m_kompareInfo->localSource = m_patch->baseDir().toLocalFile(); m_kompareInfo->depth = m_patch->depth(); m_kompareInfo->applied = m_patch->isAlreadyApplied(); m_modelList.reset( new Diff2::KompareModelList( m_diffSettings.data(), new QWidget, this ) ); m_modelList->slotKompareInfo( m_kompareInfo.data() ); try { m_modelList->openDirAndDiff(); } catch ( const QString & str ) { throw; } catch ( ... ) { throw QStringLiteral( "lib/libdiff2 crashed, memory may be corrupted. Please restart kdevelop." ); } for (m_depth = 0; m_depth < 10; ++m_depth) { bool allFound = true; for( int i = 0; i < m_modelList->modelCount(); i++ ) { if (!QFile::exists(urlForFileModel(m_modelList->modelAt(i)).toLocalFile())) { allFound = false; } } if (allFound) { break; // found depth } } emit patchChanged(); for( int i = 0; i < m_modelList->modelCount(); i++ ) { const Diff2::DiffModel* model = m_modelList->modelAt( i ); for (auto* difference : *model->differences()) { difference->apply(m_patch->isAlreadyApplied()); } } highlightPatch(); return; } catch ( const QString & str ) { KMessageBox::error( nullptr, str, i18n( "Kompare Model Update" ) ); } catch ( const char * str ) { KMessageBox::error( nullptr, QLatin1String(str), i18n( "Kompare Model Update" ) ); } removeHighlighting(); m_modelList.reset( nullptr ); m_depth = 0; m_kompareInfo.reset( nullptr ); delete m_diffSettings; emit patchChanged(); } K_PLUGIN_FACTORY_WITH_JSON(KDevPatchReviewFactory, "kdevpatchreview.json", registerPlugin();) class PatchReviewToolViewFactory : public KDevelop::IToolViewFactory { public: explicit PatchReviewToolViewFactory( PatchReviewPlugin *plugin ) : m_plugin( plugin ) {} QWidget* create( QWidget *parent = nullptr ) override { return new PatchReviewToolView( parent, m_plugin ); } Qt::DockWidgetArea defaultPosition() const override { return Qt::BottomDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.PatchReview"); } private: PatchReviewPlugin *m_plugin; }; PatchReviewPlugin::~PatchReviewPlugin() { removeHighlighting(); // Tweak to work around a crash on OS X; see https://bugs.kde.org/show_bug.cgi?id=338829 // and http://qt-project.org/forums/viewthread/38406/#162801 // modified tweak: use setPatch() and deleteLater in that method. setPatch(nullptr); } void PatchReviewPlugin::clearPatch( QObject* _patch ) { qCDebug(PLUGIN_PATCHREVIEW) << "clearing patch" << _patch << "current:" << ( QObject* )m_patch; IPatchSource::Ptr patch( ( IPatchSource* )_patch ); if( patch == m_patch ) { qCDebug(PLUGIN_PATCHREVIEW) << "is current patch"; setPatch( IPatchSource::Ptr( new LocalPatchSource ) ); } } void PatchReviewPlugin::closeReview() { if( m_patch ) { IDocument* patchDocument = ICore::self()->documentController()->documentForUrl( m_patch->file() ); if (patchDocument) { // Revert modifications to the text document which we've done in updateReview patchDocument->setPrettyName( QString() ); patchDocument->textDocument()->setReadWrite( true ); auto* modif = qobject_cast(patchDocument->textDocument()); modif->setModifiedOnDiskWarning( true ); } removeHighlighting(); m_modelList.reset( nullptr ); m_depth = 0; if (!qobject_cast(m_patch.data())) { // make sure "show" button still openes the file dialog to open a custom patch file setPatch( new LocalPatchSource ); } else emit patchChanged(); Sublime::Area* area = ICore::self()->uiController()->activeArea(); if( area->objectName() == QLatin1String("review") ) { if( ICore::self()->documentController()->saveAllDocuments() ) ICore::self()->uiController()->switchToArea( QStringLiteral("code"), KDevelop::IUiController::ThisWindow ); } } } void PatchReviewPlugin::cancelReview() { if( m_patch ) { m_patch->cancelReview(); closeReview(); } } void PatchReviewPlugin::finishReview(const QList& selection) { if( m_patch && m_patch->finishReview( selection ) ) { closeReview(); } } void PatchReviewPlugin::startReview( IPatchSource* patch, IPatchReview::ReviewMode mode ) { Q_UNUSED( mode ); emit startingNewReview(); setPatch( patch ); QMetaObject::invokeMethod( this, "updateReview", Qt::QueuedConnection ); } void PatchReviewPlugin::switchToEmptyReviewArea() { const auto allAreas = ICore::self()->uiController()->allAreas(); for (Sublime::Area* area : allAreas) { if (area->objectName() == QLatin1String("review")) { area->clearDocuments(); } } if ( ICore::self()->uiController()->activeArea()->objectName() != QLatin1String("review") ) ICore::self()->uiController()->switchToArea( QStringLiteral("review"), KDevelop::IUiController::ThisWindow ); } QUrl PatchReviewPlugin::urlForFileModel( const Diff2::DiffModel* model ) { KDevelop::Path path(QDir::cleanPath(m_patch->baseDir().toLocalFile())); QVector destPath = KDevelop::Path(QLatin1Char('/') + model->destinationPath()).segments(); if (destPath.size() >= (int)m_depth) { destPath.remove(0, m_depth); } for (const QString& segment : qAsConst(destPath)) { path.addPath(segment); } path.addPath(model->destinationFile()); return path.toUrl(); } void PatchReviewPlugin::updateReview() { if( !m_patch ) return; m_updateKompareTimer->stop(); switchToEmptyReviewArea(); KDevelop::IDocumentController *docController = ICore::self()->documentController(); // don't add documents opened automatically to the Files/Open Recent list IDocument* futureActiveDoc = docController->openDocument( m_patch->file(), KTextEditor::Range::invalid(), IDocumentController::DoNotAddToRecentOpen ); updateKompareModel(); if ( !m_modelList || !futureActiveDoc || !futureActiveDoc->textDocument() ) { // might happen if e.g. openDocument dialog was cancelled by user // or under the theoretic possibility of a non-text document getting opened return; } futureActiveDoc->textDocument()->setReadWrite( false ); futureActiveDoc->setPrettyName( i18n( "Overview" ) ); auto* modif = qobject_cast(futureActiveDoc->textDocument()); modif->setModifiedOnDiskWarning( false ); docController->activateDocument( futureActiveDoc ); auto* toolView = qobject_cast(ICore::self()->uiController()->findToolView( i18n( "Patch Review" ), m_factory )); Q_ASSERT( toolView ); //Open all relates files for( int a = 0; a < m_modelList->modelCount() && a < maximumFilesToOpenDirectly; ++a ) { QUrl absoluteUrl = urlForFileModel( m_modelList->modelAt( a ) ); if (absoluteUrl.isRelative()) { - KMessageBox::error( nullptr, i18n("The base directory of the patch must be an absolute directory"), i18n( "Patch Review" ) ); + const QString messageText = i18n("The base directory of the patch must be an absolute directory."); + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); break; } if( QFileInfo::exists( absoluteUrl.toLocalFile() ) && absoluteUrl.toLocalFile() != QLatin1String("/dev/null") ) { toolView->open( absoluteUrl, false ); }else{ // Maybe the file was deleted qCDebug(PLUGIN_PATCHREVIEW) << "could not open" << absoluteUrl << "because it doesn't exist"; } } } void PatchReviewPlugin::setPatch( IPatchSource* patch ) { if ( patch == m_patch ) { return; } if( m_patch ) { disconnect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged ); if ( qobject_cast( m_patch ) ) { // make sure we don't leak this // TODO: what about other patch sources? m_patch->deleteLater(); } } m_patch = patch; if( m_patch ) { qCDebug(PLUGIN_PATCHREVIEW) << "setting new patch" << patch->name() << "with file" << patch->file() << "basedir" << patch->baseDir(); connect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged ); } QString finishText = i18n( "Finish Review" ); if( m_patch && !m_patch->finishReviewCustomText().isEmpty() ) finishText = m_patch->finishReviewCustomText(); m_finishReview->setText( finishText ); m_finishReview->setEnabled( patch ); notifyPatchChanged(); } PatchReviewPlugin::PatchReviewPlugin( QObject *parent, const QVariantList & ) : KDevelop::IPlugin( QStringLiteral("kdevpatchreview"), parent ), m_patch( nullptr ), m_factory( new PatchReviewToolViewFactory( this ) ) { qRegisterMetaType( "const Diff2::DiffModel*" ); setXMLFile( QStringLiteral("kdevpatchreview.rc") ); connect( ICore::self()->documentController(), &IDocumentController::documentClosed, this, &PatchReviewPlugin::documentClosed ); connect( ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this, &PatchReviewPlugin::textDocumentCreated ); connect( ICore::self()->documentController(), &IDocumentController::documentSaved, this, &PatchReviewPlugin::documentSaved ); m_updateKompareTimer = new QTimer( this ); m_updateKompareTimer->setSingleShot( true ); m_updateKompareTimer->setInterval(500); connect( m_updateKompareTimer, &QTimer::timeout, this, &PatchReviewPlugin::updateKompareModel ); m_finishReview = new QAction(i18n("Finish Review"), this); m_finishReview->setIcon( QIcon::fromTheme( QStringLiteral("dialog-ok") ) ); actionCollection()->setDefaultShortcut( m_finishReview, Qt::CTRL|Qt::Key_Return ); actionCollection()->addAction(QStringLiteral("commit_or_finish_review"), m_finishReview); const auto allAreas = ICore::self()->uiController()->allAreas(); for (Sublime::Area* area : allAreas) { if (area->objectName() == QLatin1String("review")) area->addAction(m_finishReview); } core()->uiController()->addToolView( i18n( "Patch Review" ), m_factory, IUiController::None ); areaChanged(ICore::self()->uiController()->activeArea()); } void PatchReviewPlugin::documentClosed( IDocument* doc ) { removeHighlighting( doc->url() ); } void PatchReviewPlugin::documentSaved( IDocument* doc ) { // Only update if the url is not the patch-file, because our call to // the reload() KTextEditor function also causes this signal, // which would lead to an endless update loop. // Also, don't automatically update local patch sources, because // they may correspond to static files which don't match any more // after an edit was done. if (m_patch && doc->url() != m_patch->file() && !qobject_cast(m_patch.data())) { forceUpdate(); } } void PatchReviewPlugin::textDocumentCreated( IDocument* doc ) { if (m_patch) { addHighlighting( doc->url(), doc ); } } void PatchReviewPlugin::unload() { core()->uiController()->removeToolView( m_factory ); KDevelop::IPlugin::unload(); } void PatchReviewPlugin::areaChanged(Sublime::Area* area) { bool reviewing = area->objectName() == QLatin1String("review"); m_finishReview->setEnabled(reviewing); if(!reviewing) { closeReview(); } } KDevelop::ContextMenuExtension PatchReviewPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { QList urls; if ( context->type() == KDevelop::Context::FileContext ) { auto* filectx = static_cast(context); urls = filectx->urls(); } else if ( context->type() == KDevelop::Context::ProjectItemContext ) { auto* projctx = static_cast(context); const auto items = projctx->items(); for (KDevelop::ProjectBaseItem* item : items) { if ( item->file() ) { urls << item->file()->path().toUrl(); } } } else if ( context->type() == KDevelop::Context::EditorContext ) { auto* econtext = static_cast(context); urls << econtext->url(); } if (urls.size() == 1) { QAction* reviewAction = new QAction( QIcon::fromTheme(QStringLiteral("text-x-patch")), i18n("Review Patch"), parent); reviewAction->setData(QVariant(urls[0])); connect( reviewAction, &QAction::triggered, this, &PatchReviewPlugin::executeFileReviewAction ); ContextMenuExtension cm; cm.addAction( KDevelop::ContextMenuExtension::VcsGroup, reviewAction ); return cm; } return KDevelop::IPlugin::contextMenuExtension(context, parent); } void PatchReviewPlugin::executeFileReviewAction() { auto* reviewAction = qobject_cast(sender()); KDevelop::Path path(reviewAction->data().toUrl()); auto* ps = new LocalPatchSource(); ps->setFilename(path.toUrl()); ps->setBaseDir(path.parent().toUrl()); ps->setAlreadyApplied(true); ps->createWidget(); startReview(ps, OpenAndRaise); } #include "patchreview.moc" diff --git a/plugins/patchreview/patchreviewtoolview.cpp b/plugins/patchreview/patchreviewtoolview.cpp index 15dcf0fb42..f06810e41e 100644 --- a/plugins/patchreview/patchreviewtoolview.cpp +++ b/plugins/patchreview/patchreviewtoolview.cpp @@ -1,600 +1,603 @@ /*************************************************************************** Copyright 2006-2009 David Nolden ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "patchreviewtoolview.h" #include "localpatchsource.h" #include "patchreview.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include -#include #include #include #include #ifdef WITH_PURPOSE #include #include #endif using namespace KDevelop; class PatchFilesModel : public VcsFileChangesModel { Q_OBJECT public: PatchFilesModel( QObject *parent, bool allowSelection ) : VcsFileChangesModel( parent, allowSelection ) { }; enum ItemRoles { HunksNumberRole = LastItemRole+1 }; public Q_SLOTS: void updateState( const KDevelop::VcsStatusInfo &status, unsigned hunksNum ) { int row = VcsFileChangesModel::updateState( invisibleRootItem(), status ); if ( row == -1 ) return; QStandardItem *item = invisibleRootItem()->child( row, 0 ); setFileInfo( item, hunksNum ); item->setData( QVariant( hunksNum ), HunksNumberRole ); } void updateState( const KDevelop::VcsStatusInfo &status ) { int row = VcsFileChangesModel::updateState( invisibleRootItem(), status ); if ( row == -1 ) return; QStandardItem *item = invisibleRootItem()->child( row, 0 ); setFileInfo( invisibleRootItem()->child( row, 0 ), item->data( HunksNumberRole ).toUInt() ); } private: void setFileInfo( QStandardItem *item, unsigned int hunksNum ) { const auto url = item->index().data(VcsFileChangesModel::UrlRole).toUrl(); const QString path = ICore::self()->projectController()->prettyFileName(url, KDevelop::IProjectController::FormatPlain); const QString newText = i18ncp( "%1: number of changed hunks, %2: file name", "%2 (1 hunk)", "%2 (%1 hunks)", hunksNum, path); item->setText( newText ); } }; PatchReviewToolView::PatchReviewToolView( QWidget* parent, PatchReviewPlugin* plugin ) : QWidget( parent ), m_resetCheckedUrls( true ), m_plugin( plugin ) { setWindowIcon(QIcon::fromTheme(QStringLiteral("text-x-patch"), windowIcon())); connect( m_plugin->finishReviewAction(), &QAction::triggered, this, &PatchReviewToolView::finishReview ); connect( plugin, &PatchReviewPlugin::patchChanged, this, &PatchReviewToolView::patchChanged ); connect( plugin, &PatchReviewPlugin::startingNewReview, this, &PatchReviewToolView::startingNewReview ); connect( ICore::self()->documentController(), &IDocumentController::documentActivated, this, &PatchReviewToolView::documentActivated ); auto* w = qobject_cast(ICore::self()->uiController()->activeMainWindow()); connect(w, &Sublime::MainWindow::areaChanged, m_plugin, &PatchReviewPlugin::areaChanged); showEditDialog(); patchChanged(); } void PatchReviewToolView::resizeEvent(QResizeEvent* ev) { bool vertical = (width() < height()); m_editPatch.buttonsLayout->setDirection(vertical ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight); m_editPatch.contentLayout->setDirection(vertical ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight); m_editPatch.buttonsSpacer->changeSize(vertical ? 0 : 40, 0, QSizePolicy::Fixed, QSizePolicy::Fixed); QWidget::resizeEvent(ev); if(m_customWidget) { m_editPatch.contentLayout->removeWidget( m_customWidget ); m_editPatch.contentLayout->insertWidget(0, m_customWidget ); } } void PatchReviewToolView::startingNewReview() { m_resetCheckedUrls = true; } void PatchReviewToolView::patchChanged() { fillEditFromPatch(); kompareModelChanged(); #ifdef WITH_PURPOSE IPatchSource::Ptr p = m_plugin->patch(); if (p) { m_exportMenu->model()->setInputData(QJsonObject { { QStringLiteral("urls"), QJsonArray { p->file().toString() } }, { QStringLiteral("mimeType"), { QStringLiteral("text/x-patch") } }, { QStringLiteral("localBaseDir"), { p->baseDir().toString() } }, { QStringLiteral("updateComment"), { QStringLiteral("Patch updated through KDevelop's Patch Review plugin") } } }); } #endif } PatchReviewToolView::~PatchReviewToolView() { } LocalPatchSource* PatchReviewToolView::GetLocalPatchSource() { IPatchSource::Ptr ips = m_plugin->patch(); if ( !ips ) return nullptr; return qobject_cast(ips.data()); } void PatchReviewToolView::fillEditFromPatch() { IPatchSource::Ptr ipatch = m_plugin->patch(); if ( !ipatch ) return; m_editPatch.cancelReview->setVisible( ipatch->canCancel() ); m_fileModel->setIsCheckbable( m_plugin->patch()->canSelectFiles() ); if( m_customWidget ) { qCDebug(PLUGIN_PATCHREVIEW) << "removing custom widget"; m_customWidget->hide(); m_editPatch.contentLayout->removeWidget( m_customWidget ); } m_customWidget = ipatch->customWidget(); if( m_customWidget ) { m_editPatch.contentLayout->insertWidget( 0, m_customWidget ); m_customWidget->show(); qCDebug(PLUGIN_PATCHREVIEW) << "got custom widget"; } bool showTests = false; QMap files = ipatch->additionalSelectableFiles(); QMap::const_iterator it = files.constBegin(); for (; it != files.constEnd(); ++it) { auto project = ICore::self()->projectController()->findProjectForUrl(it.key()); if (project && !ICore::self()->testController()->testSuitesForProject(project).isEmpty()) { showTests = true; break; } } m_editPatch.testsButton->setVisible(showTests); m_editPatch.testProgressBar->hide(); } void PatchReviewToolView::slotAppliedChanged( int newState ) { if ( LocalPatchSource* lpatch = GetLocalPatchSource() ) { lpatch->setAlreadyApplied( newState == Qt::Checked ); m_plugin->notifyPatchChanged(); } } void PatchReviewToolView::showEditDialog() { m_editPatch.setupUi( this ); bool allowSelection = m_plugin->patch() && m_plugin->patch()->canSelectFiles(); m_fileModel = new PatchFilesModel( this, allowSelection ); m_fileSortProxyModel = new VcsFileChangesSortProxyModel(this); m_fileSortProxyModel->setSourceModel(m_fileModel); m_fileSortProxyModel->sort(1); m_fileSortProxyModel->setDynamicSortFilter(true); m_editPatch.filesList->setModel( m_fileSortProxyModel ); m_editPatch.filesList->header()->hide(); m_editPatch.filesList->setRootIsDecorated( false ); m_editPatch.filesList->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_editPatch.filesList, &QTreeView::customContextMenuRequested, this, &PatchReviewToolView::customContextMenuRequested); connect(m_fileModel, &PatchFilesModel::itemChanged, this, &PatchReviewToolView::fileItemChanged); m_editPatch.finishReview->setDefaultAction(m_plugin->finishReviewAction()); #ifdef WITH_PURPOSE m_exportMenu = new Purpose::Menu(this); - connect(m_exportMenu, &Purpose::Menu::finished, this, [](const QJsonObject &output, int error, const QString &message) { + connect(m_exportMenu, &Purpose::Menu::finished, this, [](const QJsonObject &output, int error, const QString &errorMessage) { + Sublime::Message* message; if (error==0) { - KMessageBox::information(nullptr, i18n("You can find the new request at:
%1
", output[QLatin1String("url")].toString()), - QString(), QString(), KMessageBox::AllowLink); + const QString messageText = i18n("You can find the new request at:
%1
", output[QLatin1String("url")].toString()); + message = new Sublime::Message(messageText, Sublime::Message::Information); } else { - QMessageBox::warning(nullptr, i18n("Error exporting"), i18n("Couldn't export the patch.\n%1", message)); + const QString messageText = i18n("Couldn't export the patch.\n%1", errorMessage); + message = new Sublime::Message(messageText, Sublime::Message::Error); } + ICore::self()->uiController()->postMessage(message); }); // set the model input parameters to avoid terminal warnings m_exportMenu->model()->setInputData(QJsonObject { { QStringLiteral("urls"), QJsonArray { QString() } }, { QStringLiteral("mimeType"), { QStringLiteral("text/x-patch") } } }); m_exportMenu->model()->setPluginType(QStringLiteral("Export")); m_editPatch.exportReview->setMenu( m_exportMenu ); #else m_editPatch.exportReview->setEnabled(false); #endif connect( m_editPatch.previousHunk, &QToolButton::clicked, this, &PatchReviewToolView::prevHunk ); connect( m_editPatch.nextHunk, &QToolButton::clicked, this, &PatchReviewToolView::nextHunk ); connect( m_editPatch.previousFile, &QToolButton::clicked, this, &PatchReviewToolView::prevFile ); connect( m_editPatch.nextFile, &QToolButton::clicked, this, &PatchReviewToolView::nextFile ); connect( m_editPatch.filesList, &QTreeView::activated , this, &PatchReviewToolView::fileDoubleClicked ); connect( m_editPatch.cancelReview, &QPushButton::clicked, m_plugin, &PatchReviewPlugin::cancelReview ); //connect( m_editPatch.cancelButton, SIGNAL(pressed()), this, SLOT(slotEditCancel()) ); //connect( this, SIGNAL(finished(int)), this, SLOT(slotEditDialogFinished(int)) ); connect( m_editPatch.updateButton, &QPushButton::clicked, m_plugin, &PatchReviewPlugin::forceUpdate ); connect( m_editPatch.testsButton, &QPushButton::clicked, this, &PatchReviewToolView::runTests ); m_selectAllAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-select-all")), i18n("Select All"), this ); connect( m_selectAllAction, &QAction::triggered, this, &PatchReviewToolView::selectAll ); m_deselectAllAction = new QAction( i18n("Deselect All"), this ); connect( m_deselectAllAction, &QAction::triggered, this, &PatchReviewToolView::deselectAll ); } void PatchReviewToolView::customContextMenuRequested(const QPoint& pos) { QList urls; const QModelIndexList selectionIdxs = m_editPatch.filesList->selectionModel()->selectedIndexes(); urls.reserve(selectionIdxs.size()); for (const QModelIndex& idx : selectionIdxs) { urls += idx.data(KDevelop::VcsFileChangesModel::UrlRole).toUrl(); } QPointer menu = new QMenu(m_editPatch.filesList); QList extensions; if(!urls.isEmpty()) { KDevelop::FileContext context(urls); extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, menu); } QList vcsActions; for (const ContextMenuExtension& ext : qAsConst(extensions)) { vcsActions += ext.actions(ContextMenuExtension::VcsGroup); } menu->addAction(m_selectAllAction); menu->addAction(m_deselectAllAction); menu->addActions(vcsActions); menu->exec(m_editPatch.filesList->viewport()->mapToGlobal(pos)); delete menu; } void PatchReviewToolView::nextHunk() { IDocument* current = ICore::self()->documentController()->activeDocument(); if(current && current->textDocument()) m_plugin->seekHunk( true, current->textDocument()->url() ); } void PatchReviewToolView::prevHunk() { IDocument* current = ICore::self()->documentController()->activeDocument(); if(current && current->textDocument()) m_plugin->seekHunk( false, current->textDocument()->url() ); } void PatchReviewToolView::seekFile(bool forwards) { if(!m_plugin->patch()) return; QList checkedUrls = m_fileModel->checkedUrls(); QList allUrls = m_fileModel->urls(); IDocument* current = ICore::self()->documentController()->activeDocument(); if(!current || checkedUrls.empty()) return; qCDebug(PLUGIN_PATCHREVIEW) << "seeking direction" << forwards; int currentIndex = allUrls.indexOf(current->url()); QUrl newUrl; if((forwards && current->url() == checkedUrls.back()) || (!forwards && current->url() == checkedUrls[0])) { newUrl = m_plugin->patch()->file(); qCDebug(PLUGIN_PATCHREVIEW) << "jumping to patch"; } else if(current->url() == m_plugin->patch()->file() || currentIndex == -1) { if(forwards) newUrl = checkedUrls[0]; else newUrl = checkedUrls.back(); qCDebug(PLUGIN_PATCHREVIEW) << "jumping from patch"; } else { QSet checkedUrlsSet( checkedUrls.toSet() ); for(int offset = 1; offset < allUrls.size(); ++offset) { int pos; if(forwards) { pos = (currentIndex + offset) % allUrls.size(); }else{ pos = currentIndex - offset; if(pos < 0) pos += allUrls.size(); } if(checkedUrlsSet.contains(allUrls[pos])) { newUrl = allUrls[pos]; break; } } } if(newUrl.isValid()) { open( newUrl, true ); }else{ qCDebug(PLUGIN_PATCHREVIEW) << "found no valid target url"; } } void PatchReviewToolView::open( const QUrl& url, bool activate ) const { qCDebug(PLUGIN_PATCHREVIEW) << "activating url" << url; // If the document is already open in this area, just re-activate it if(KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url)) { const auto views = ICore::self()->uiController()->activeArea()->views(); for (Sublime::View* view : views) { if(view->document() == dynamic_cast(doc)) { if (activate) { // use openDocument() for the activation so that the document is added to File/Open Recent. ICore::self()->documentController()->openDocument(doc->url(), KTextEditor::Range::invalid()); } return; } } } QStandardItem* item = m_fileModel->itemForUrl( url ); IDocument* buddyDoc = nullptr; if (m_plugin->patch() && item) { for (int preRow = item->row() - 1; preRow >= 0; --preRow) { QStandardItem* preItem = m_fileModel->item(preRow); if (!m_fileModel->isCheckable() || preItem->checkState() == Qt::Checked) { // found valid predecessor, take it as buddy buddyDoc = ICore::self()->documentController()->documentForUrl(preItem->index().data(VcsFileChangesModel::UrlRole).toUrl()); if (buddyDoc) { break; } } } if (!buddyDoc) { buddyDoc = ICore::self()->documentController()->documentForUrl(m_plugin->patch()->file()); } } // we simplify and assume that documents to be opened without activating them also need not be // added to the Files/Open Recent menu. IDocument* newDoc = ICore::self()->documentController()->openDocument(url, KTextEditor::Range::invalid(), activate ? IDocumentController::DefaultMode : IDocumentController::DoNotActivate|IDocumentController::DoNotAddToRecentOpen, QString(), buddyDoc); KTextEditor::View* view = nullptr; if(newDoc) view = newDoc->activeTextView(); if(view && view->cursorPosition().line() == 0) m_plugin->seekHunk( true, url ); } void PatchReviewToolView::fileItemChanged( QStandardItem* item ) { if (item->column() != 0 || !m_plugin->patch()) return; QUrl url = item->index().data(VcsFileChangesModel::UrlRole).toUrl(); if (url.isEmpty()) return; KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if(m_fileModel->isCheckable() && item->checkState() != Qt::Checked) { // The file was deselected, so eventually close it if(doc && doc->state() == IDocument::Clean) { const auto views = ICore::self()->uiController()->activeArea()->views(); for (Sublime::View* view : views) { if(view->document() == dynamic_cast(doc)) { ICore::self()->uiController()->activeArea()->closeView(view); return; } } } } else if (!doc) { // Maybe the file was unchecked before, or it was just loaded. open( url, false ); } } void PatchReviewToolView::nextFile() { seekFile(true); } void PatchReviewToolView::prevFile() { seekFile(false); } void PatchReviewToolView::deselectAll() { m_fileModel->setAllChecked(false); } void PatchReviewToolView::selectAll() { m_fileModel->setAllChecked(true); } void PatchReviewToolView::finishReview() { QList selectedUrls = m_fileModel->checkedUrls(); qCDebug(PLUGIN_PATCHREVIEW) << "finishing review with" << selectedUrls; m_plugin->finishReview( selectedUrls ); } void PatchReviewToolView::fileDoubleClicked( const QModelIndex& idx ) { const QUrl file = idx.data(VcsFileChangesModel::UrlRole).toUrl(); open( file, true ); } void PatchReviewToolView::kompareModelChanged() { QList oldCheckedUrls = m_fileModel->checkedUrls(); m_fileModel->clear(); if ( !m_plugin->modelList() ) return; QMap additionalUrls = m_plugin->patch()->additionalSelectableFiles(); const Diff2::DiffModelList* models = m_plugin->modelList()->models(); if( models ) { for (auto* model : *models) { const Diff2::DifferenceList* diffs = model->differences(); int cnt = 0; if ( diffs ) cnt = diffs->count(); const QUrl file = m_plugin->urlForFileModel(model); if( file.isLocalFile() && !QFileInfo( file.toLocalFile() ).isReadable() ) continue; VcsStatusInfo status; status.setUrl( file ); status.setState( cnt>0 ? VcsStatusInfo::ItemModified : VcsStatusInfo::ItemUpToDate ); m_fileModel->updateState( status, cnt ); } } for( QMap::const_iterator it = additionalUrls.constBegin(); it != additionalUrls.constEnd(); ++it ) { VcsStatusInfo status; status.setUrl( it.key() ); status.setState( it.value() ); m_fileModel->updateState( status ); } if(!m_resetCheckedUrls) m_fileModel->setCheckedUrls(oldCheckedUrls); else m_resetCheckedUrls = false; m_editPatch.filesList->resizeColumnToContents( 0 ); // Eventually select the active document documentActivated( ICore::self()->documentController()->activeDocument() ); } void PatchReviewToolView::documentActivated( IDocument* doc ) { if( !doc ) return; if ( !m_plugin->modelList() ) return; const auto matches = m_fileSortProxyModel->match( m_fileSortProxyModel->index(0, 0), VcsFileChangesModel::UrlRole, doc->url(), 1, Qt::MatchExactly); m_editPatch.filesList->setCurrentIndex(matches.value(0)); } void PatchReviewToolView::runTests() { IPatchSource::Ptr ipatch = m_plugin->patch(); if ( !ipatch ) { return; } IProject* project = nullptr; QMap files = ipatch->additionalSelectableFiles(); QMap::const_iterator it = files.constBegin(); for (; it != files.constEnd(); ++it) { project = ICore::self()->projectController()->findProjectForUrl(it.key()); if (project) { break; } } if (!project) { return; } m_editPatch.testProgressBar->setFormat(i18n("Running tests: %p%")); m_editPatch.testProgressBar->setValue(0); m_editPatch.testProgressBar->show(); auto* job = new ProjectTestJob(project, this); connect(job, &ProjectTestJob::finished, this, &PatchReviewToolView::testJobResult); connect(job, SIGNAL(percent(KJob*,ulong)), this, SLOT(testJobPercent(KJob*,ulong))); ICore::self()->runController()->registerJob(job); } void PatchReviewToolView::testJobPercent(KJob* job, ulong percent) { Q_UNUSED(job); m_editPatch.testProgressBar->setValue(percent); } void PatchReviewToolView::testJobResult(KJob* job) { auto* testJob = qobject_cast(job); if (!testJob) { return; } ProjectTestResult result = testJob->testResult(); QString format; if (result.passed > 0 && result.failed == 0 && result.error == 0) { format = i18np("Test passed", "All %1 tests passed", result.passed); } else { format = i18n("Test results: %1 passed, %2 failed, %3 errors", result.passed, result.failed, result.error); } m_editPatch.testProgressBar->setFormat(format); // Needed because some test jobs may raise their own output views ICore::self()->uiController()->raiseToolView(this); } #include "patchreviewtoolview.moc" diff --git a/plugins/projectmanagerview/projectmanagerviewplugin.cpp b/plugins/projectmanagerview/projectmanagerviewplugin.cpp index f68b5c979b..161ff11d14 100644 --- a/plugins/projectmanagerview/projectmanagerviewplugin.cpp +++ b/plugins/projectmanagerview/projectmanagerviewplugin.cpp @@ -1,795 +1,801 @@ /* This file is part of KDevelop Copyright 2004 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2016, 2017 Alexander Potashev 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 "projectmanagerviewplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include "projectmanagerview.h" #include "debug.h" #include "cutcopypastehelpers.h" using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(ProjectManagerFactory, "kdevprojectmanagerview.json", registerPlugin();) namespace { QAction* createSeparatorAction() { auto* separator = new QAction(nullptr); separator->setSeparator(true); return separator; } // Returns nullptr iff the list of URLs to copy/cut was empty QMimeData* createClipboardMimeData(const bool cut) { auto* ctx = dynamic_cast( ICore::self()->selectionController()->currentSelection()); QList urls; QList mostLocalUrls; const auto& items = ctx->items(); for (const ProjectBaseItem* item : items) { if (item->folder() || item->file()) { const QUrl& url = item->path().toUrl(); urls << url; mostLocalUrls << KFileItem(url).mostLocalUrl(); } } qCDebug(PLUGIN_PROJECTMANAGERVIEW) << urls; if (urls.isEmpty()) { return nullptr; } auto* mimeData = new QMimeData; KIO::setClipboardDataCut(mimeData, cut); KUrlMimeData::setUrls(urls, mostLocalUrls, mimeData); return mimeData; } } // anonymous namespace class KDevProjectManagerViewFactory: public KDevelop::IToolViewFactory { public: explicit KDevProjectManagerViewFactory( ProjectManagerViewPlugin *plugin ): mplugin( plugin ) {} QWidget* create( QWidget *parent = nullptr ) override { return new ProjectManagerView( mplugin, parent ); } Qt::DockWidgetArea defaultPosition() const override { return Qt::LeftDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.ProjectsView"); } private: ProjectManagerViewPlugin *mplugin; }; class ProjectManagerViewPluginPrivate { public: ProjectManagerViewPluginPrivate() {} KDevProjectManagerViewFactory *factory; QList ctxProjectItemList; QAction* m_buildAll; QAction* m_build; QAction* m_install; QAction* m_clean; QAction* m_configure; QAction* m_prune; }; static QList itemsFromIndexes(const QList& indexes) { QList items; ProjectModel* model = ICore::self()->projectController()->projectModel(); items.reserve(indexes.size()); for (const QModelIndex& index : indexes) { items += model->itemFromIndex(index); } return items; } ProjectManagerViewPlugin::ProjectManagerViewPlugin( QObject *parent, const QVariantList& ) : IPlugin( QStringLiteral("kdevprojectmanagerview"), parent ), d(new ProjectManagerViewPluginPrivate) { d->m_buildAll = new QAction( i18n("Build all Projects"), this ); d->m_buildAll->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); connect( d->m_buildAll, &QAction::triggered, this, &ProjectManagerViewPlugin::buildAllProjects ); actionCollection()->addAction( QStringLiteral("project_buildall"), d->m_buildAll ); d->m_build = new QAction( i18n("Build Selection"), this ); d->m_build->setIconText( i18n("Build") ); actionCollection()->setDefaultShortcut( d->m_build, Qt::Key_F8 ); d->m_build->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); d->m_build->setEnabled( false ); connect( d->m_build, &QAction::triggered, this, &ProjectManagerViewPlugin::buildProjectItems ); actionCollection()->addAction( QStringLiteral("project_build"), d->m_build ); d->m_install = new QAction( i18n("Install Selection"), this ); d->m_install->setIconText( i18n("Install") ); d->m_install->setIcon(QIcon::fromTheme(QStringLiteral("run-build-install"))); actionCollection()->setDefaultShortcut( d->m_install, Qt::SHIFT + Qt::Key_F8 ); d->m_install->setEnabled( false ); connect( d->m_install, &QAction::triggered, this, &ProjectManagerViewPlugin::installProjectItems ); actionCollection()->addAction( QStringLiteral("project_install"), d->m_install ); d->m_clean = new QAction( i18n("Clean Selection"), this ); d->m_clean->setIconText( i18n("Clean") ); d->m_clean->setIcon(QIcon::fromTheme(QStringLiteral("run-build-clean"))); d->m_clean->setEnabled( false ); connect( d->m_clean, &QAction::triggered, this, &ProjectManagerViewPlugin::cleanProjectItems ); actionCollection()->addAction( QStringLiteral("project_clean"), d->m_clean ); d->m_configure = new QAction( i18n("Configure Selection"), this ); d->m_configure->setMenuRole( QAction::NoRole ); // OSX: Be explicit about role, prevent hiding due to conflict with "Preferences..." menu item d->m_configure->setIconText( i18n("Configure") ); d->m_configure->setIcon(QIcon::fromTheme(QStringLiteral("run-build-configure"))); d->m_configure->setEnabled( false ); connect( d->m_configure, &QAction::triggered, this, &ProjectManagerViewPlugin::configureProjectItems ); actionCollection()->addAction( QStringLiteral("project_configure"), d->m_configure ); d->m_prune = new QAction( i18n("Prune Selection"), this ); d->m_prune->setIconText( i18n("Prune") ); d->m_prune->setIcon(QIcon::fromTheme(QStringLiteral("run-build-prune"))); d->m_prune->setEnabled( false ); connect( d->m_prune, &QAction::triggered, this, &ProjectManagerViewPlugin::pruneProjectItems ); actionCollection()->addAction( QStringLiteral("project_prune"), d->m_prune ); // only add the action so that its known in the actionCollection // and so that it's shortcut etc. pp. is restored // apparently that is not possible to be done in the view itself *sigh* actionCollection()->addAction( QStringLiteral("locate_document") ); setXMLFile( QStringLiteral("kdevprojectmanagerview.rc") ); d->factory = new KDevProjectManagerViewFactory( this ); core()->uiController()->addToolView( i18n("Projects"), d->factory ); connect(core()->selectionController(), &ISelectionController::selectionChanged, this, &ProjectManagerViewPlugin::updateActionState); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::rowsInserted, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::rowsRemoved, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::modelReset, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); } void ProjectManagerViewPlugin::updateFromBuildSetChange() { updateActionState( core()->selectionController()->currentSelection() ); } void ProjectManagerViewPlugin::updateActionState( KDevelop::Context* ctx ) { bool isEmpty = ICore::self()->projectController()->buildSetModel()->items().isEmpty(); if( isEmpty ) { isEmpty = !ctx || ctx->type() != Context::ProjectItemContext || static_cast(ctx)->items().isEmpty(); } d->m_build->setEnabled( !isEmpty ); d->m_install->setEnabled( !isEmpty ); d->m_clean->setEnabled( !isEmpty ); d->m_configure->setEnabled( !isEmpty ); d->m_prune->setEnabled( !isEmpty ); } ProjectManagerViewPlugin::~ProjectManagerViewPlugin() { delete d; } void ProjectManagerViewPlugin::unload() { qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "unloading manager view"; core()->uiController()->removeToolView(d->factory); } ContextMenuExtension ProjectManagerViewPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { if( context->type() != KDevelop::Context::ProjectItemContext ) return IPlugin::contextMenuExtension(context, parent); auto* ctx = static_cast(context); const QList items = ctx->items(); d->ctxProjectItemList.clear(); if( items.isEmpty() ) return IPlugin::contextMenuExtension(context, parent); //TODO: also needs: removeTarget, removeFileFromTarget, runTargetsFromContextMenu ContextMenuExtension menuExt; bool needsCreateFile = true; bool needsCreateFolder = true; bool needsCloseProjects = true; bool needsBuildItems = true; bool needsFolderItems = true; bool needsCutRenameRemove = true; bool needsRemoveTargetFiles = true; bool needsPaste = true; //needsCreateFile if there is one item and it's a folder or target needsCreateFile &= (items.count() == 1) && (items.first()->folder() || items.first()->target()); //needsCreateFolder if there is one item and it's a folder needsCreateFolder &= (items.count() == 1) && (items.first()->folder()); needsPaste = needsCreateFolder; d->ctxProjectItemList.reserve(items.size()); for (ProjectBaseItem* item : items) { d->ctxProjectItemList << item->index(); //needsBuildItems if items are limited to targets and buildfolders needsBuildItems &= item->target() || item->type() == ProjectBaseItem::BuildFolder; //needsCloseProjects if items are limited to top level folders (Project Folders) needsCloseProjects &= item->folder() && !item->folder()->parent(); //needsFolderItems if items are limited to folders needsFolderItems &= (bool)item->folder(); //needsRemove if items are limited to non-top-level folders or files that don't belong to targets needsCutRenameRemove &= (item->folder() && item->parent()) || (item->file() && !item->parent()->target()); //needsRemoveTargets if items are limited to file items with target parents needsRemoveTargetFiles &= (item->file() && item->parent()->target()); } if ( needsCreateFile ) { QAction* action = new QAction(i18n("Create &File..."), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::createFileFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } if ( needsCreateFolder ) { QAction* action = new QAction(i18n("Create F&older..."), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("folder-new"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::createFolderFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } if ( needsBuildItems ) { QAction* action = new QAction(i18nc("@action", "&Build"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::buildItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18nc("@action", "&Install"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build-install"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::installItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18nc("@action", "&Clean"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build-clean"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::cleanItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18n("&Add to Build Set"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::addItemsFromContextMenuToBuildset ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); } if ( needsCloseProjects ) { QAction* close = new QAction(i18np("C&lose Project", "Close Projects", items.count()), parent); close->setIcon(QIcon::fromTheme(QStringLiteral("project-development-close"))); connect( close, &QAction::triggered, this, &ProjectManagerViewPlugin::closeProjects ); menuExt.addAction( ContextMenuExtension::ProjectGroup, close ); } if ( needsFolderItems ) { QAction* action = new QAction(i18n("&Reload"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::reloadFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } // Populating cut/copy/paste group if ( !menuExt.actions(ContextMenuExtension::FileGroup).isEmpty() ) { menuExt.addAction( ContextMenuExtension::FileGroup, createSeparatorAction() ); } if ( needsCutRenameRemove ) { QAction* cut = KStandardAction::cut(this, SLOT(cutFromContextMenu()), this); cut->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction(ContextMenuExtension::FileGroup, cut); } { QAction* copy = KStandardAction::copy(this, SLOT(copyFromContextMenu()), this); copy->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction( ContextMenuExtension::FileGroup, copy ); } if (needsPaste) { QAction* paste = KStandardAction::paste(this, SLOT(pasteFromContextMenu()), this); paste->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction( ContextMenuExtension::FileGroup, paste ); } // Populating rename/remove group { menuExt.addAction( ContextMenuExtension::FileGroup, createSeparatorAction() ); } if ( needsCutRenameRemove ) { QAction* remove = new QAction(i18n("Remo&ve"), parent); remove->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); connect( remove, &QAction::triggered, this, &ProjectManagerViewPlugin::removeFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, remove ); QAction* rename = new QAction(i18n("Re&name..."), parent); rename->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); connect( rename, &QAction::triggered, this, &ProjectManagerViewPlugin::renameItemFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, rename ); } if ( needsRemoveTargetFiles ) { QAction* remove = new QAction(i18n("Remove From &Target"), parent); remove->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); connect( remove, &QAction::triggered, this, &ProjectManagerViewPlugin::removeTargetFilesFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, remove ); } if ( needsCutRenameRemove || needsRemoveTargetFiles ) { menuExt.addAction(ContextMenuExtension::FileGroup, createSeparatorAction()); } return menuExt; } void ProjectManagerViewPlugin::closeProjects() { QList projectsToClose; ProjectModel* model = ICore::self()->projectController()->projectModel(); for (const QModelIndex& index : qAsConst(d->ctxProjectItemList)) { KDevelop::ProjectBaseItem* item = model->itemFromIndex(index); if( !projectsToClose.contains( item->project() ) ) { projectsToClose << item->project(); } } d->ctxProjectItemList.clear(); for (KDevelop::IProject* proj : qAsConst(projectsToClose)) { core()->projectController()->closeProject( proj ); } } void ProjectManagerViewPlugin::installItemsFromContextMenu() { runBuilderJob( BuilderJob::Install, itemsFromIndexes(d->ctxProjectItemList) ); d->ctxProjectItemList.clear(); } void ProjectManagerViewPlugin::cleanItemsFromContextMenu() { runBuilderJob( BuilderJob::Clean, itemsFromIndexes( d->ctxProjectItemList ) ); d->ctxProjectItemList.clear(); } void ProjectManagerViewPlugin::buildItemsFromContextMenu() { runBuilderJob( BuilderJob::Build, itemsFromIndexes( d->ctxProjectItemList ) ); d->ctxProjectItemList.clear(); } QList ProjectManagerViewPlugin::collectAllProjects() { QList items; const auto projects = core()->projectController()->projects(); items.reserve(projects.size()); for (auto* project : projects) { items << project->projectItem(); } return items; } void ProjectManagerViewPlugin::buildAllProjects() { runBuilderJob( BuilderJob::Build, collectAllProjects() ); } QList ProjectManagerViewPlugin::collectItems() { QList items; const QList buildItems = ICore::self()->projectController()->buildSetModel()->items(); if( !buildItems.isEmpty() ) { for (const BuildItem& buildItem : buildItems) { if( ProjectBaseItem* item = buildItem.findItem() ) { items << item; } } } else { auto* ctx = static_cast(ICore::self()->selectionController()->currentSelection()); items = ctx->items(); } return items; } void ProjectManagerViewPlugin::runBuilderJob( BuilderJob::BuildType type, const QList& items ) { auto* builder = new BuilderJob; builder->addItems( type, items ); builder->updateJobName(); ICore::self()->uiController()->registerStatus(new JobStatus(builder)); ICore::self()->runController()->registerJob( builder ); } void ProjectManagerViewPlugin::installProjectItems() { runBuilderJob( KDevelop::BuilderJob::Install, collectItems() ); } void ProjectManagerViewPlugin::pruneProjectItems() { runBuilderJob( KDevelop::BuilderJob::Prune, collectItems() ); } void ProjectManagerViewPlugin::configureProjectItems() { runBuilderJob( KDevelop::BuilderJob::Configure, collectItems() ); } void ProjectManagerViewPlugin::cleanProjectItems() { runBuilderJob( KDevelop::BuilderJob::Clean, collectItems() ); } void ProjectManagerViewPlugin::buildProjectItems() { runBuilderJob( KDevelop::BuilderJob::Build, collectItems() ); } void ProjectManagerViewPlugin::addItemsFromContextMenuToBuildset( ) { const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { ICore::self()->projectController()->buildSetModel()->addProjectItem( item ); } } void ProjectManagerViewPlugin::runTargetsFromContextMenu( ) { const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { KDevelop::ProjectExecutableTargetItem* t=item->executable(); if(t) { qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "Running target: " << t->text() << t->builtUrl(); } } } void ProjectManagerViewPlugin::projectConfiguration( ) { if( !d->ctxProjectItemList.isEmpty() ) { ProjectModel* model = ICore::self()->projectController()->projectModel(); core()->projectController()->configureProject( model->itemFromIndex(d->ctxProjectItemList.at( 0 ))->project() ); } } void ProjectManagerViewPlugin::reloadFromContextMenu( ) { QList< KDevelop::ProjectFolderItem* > folders; const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { if ( item->folder() ) { // since reloading should be recursive, only pass the upper-most items bool found = false; const auto currentFolders = folders; for (KDevelop::ProjectFolderItem* existing : currentFolders) { if ( existing->path().isParentOf(item->folder()->path()) ) { // simply skip this child found = true; break; } else if ( item->folder()->path().isParentOf(existing->path()) ) { // remove the child in the list and add the current item instead folders.removeOne(existing); // continue since there could be more than one existing child } } if ( !found ) { folders << item->folder(); } } } for (KDevelop::ProjectFolderItem* folder : qAsConst(folders)) { folder->project()->projectFileManager()->reload(folder); } } void ProjectManagerViewPlugin::createFolderFromContextMenu( ) { const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { if ( item->folder() ) { QWidget* window(ICore::self()->uiController()->activeMainWindow()->window()); QString name = QInputDialog::getText ( window, i18n ( "Create Folder in %1", item->folder()->path().pathOrUrl() ), i18n ( "Folder name:" ) ); if (!name.isEmpty()) { item->project()->projectFileManager()->addFolder( Path(item->path(), name), item->folder() ); } } } } void ProjectManagerViewPlugin::removeFromContextMenu() { removeItems(itemsFromIndexes( d->ctxProjectItemList )); } void ProjectManagerViewPlugin::removeItems(const QList< ProjectBaseItem* >& items) { if (items.isEmpty()) { return; } //copy the list of selected items and sort it to guarantee parents will come before children QList sortedItems = items; std::sort(sortedItems.begin(), sortedItems.end(), ProjectBaseItem::pathLessThan); Path lastFolder; QHash< IProjectFileManager*, QList > filteredItems; QStringList itemPaths; for (KDevelop::ProjectBaseItem* item : qAsConst(sortedItems)) { if (item->isProjectRoot()) { continue; } else if (item->folder() || item->file()) { //make sure no children of folders that will be deleted are listed if (lastFolder.isParentOf(item->path())) { continue; } else if (item->folder()) { lastFolder = item->path(); } IProjectFileManager* manager = item->project()->projectFileManager(); if (manager) { filteredItems[manager] << item; itemPaths << item->path().pathOrUrl(); } } } if (filteredItems.isEmpty()) { return; } if (KMessageBox::warningYesNoList( QApplication::activeWindow(), i18np("Do you really want to delete this item?", "Do you really want to delete these %1 items?", itemPaths.size()), itemPaths, i18n("Delete Files"), KStandardGuiItem::del(), KStandardGuiItem::cancel() ) == KMessageBox::No) { return; } //Go though projectmanagers, have them remove the files and folders that they own QHash< IProjectFileManager*, QList >::iterator it; for (it = filteredItems.begin(); it != filteredItems.end(); ++it) { Q_ASSERT(it.key()); it.key()->removeFilesAndFolders(it.value()); } } void ProjectManagerViewPlugin::removeTargetFilesFromContextMenu() { const QList items = itemsFromIndexes( d->ctxProjectItemList ); QHash< IBuildSystemManager*, QList > itemsByBuildSystem; for (ProjectBaseItem* item : items) { itemsByBuildSystem[item->project()->buildSystemManager()].append(item->file()); } QHash< IBuildSystemManager*, QList >::iterator it; for (it = itemsByBuildSystem.begin(); it != itemsByBuildSystem.end(); ++it) it.key()->removeFilesFromTargets(it.value()); } void ProjectManagerViewPlugin::renameItemFromContextMenu() { renameItems(itemsFromIndexes( d->ctxProjectItemList )); } void ProjectManagerViewPlugin::renameItems(const QList< ProjectBaseItem* >& items) { if (items.isEmpty()) { return; } QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); for (KDevelop::ProjectBaseItem* item : items) { if ((item->type()!=ProjectBaseItem::BuildFolder && item->type()!=ProjectBaseItem::Folder && item->type()!=ProjectBaseItem::File) || !item->parent()) { continue; } const QString src = item->text(); //Change QInputDialog->KFileSaveDialog? QString name = QInputDialog::getText( window, i18n("Rename..."), i18n("New name for '%1':", item->text()), QLineEdit::Normal, item->text() ); if (!name.isEmpty() && name != src) { ProjectBaseItem::RenameStatus status = item->rename( name ); + QString errorMessageText; switch(status) { case ProjectBaseItem::RenameOk: break; case ProjectBaseItem::ExistingItemSameName: - KMessageBox::error(window, i18n("There is already a file named '%1'", name)); + errorMessageText = i18n("There is already a file named '%1'", name); break; case ProjectBaseItem::ProjectManagerRenameFailed: - KMessageBox::error(window, i18n("Could not rename '%1'", name)); + errorMessageText = i18n("Could not rename '%1'", name); break; case ProjectBaseItem::InvalidNewName: - KMessageBox::error(window, i18n("'%1' is not a valid file name", name)); + errorMessageText = i18n("'%1' is not a valid file name", name); break; } + if (!errorMessageText.isEmpty()) { + auto* message = new Sublime::Message(errorMessageText, Sublime::Message::Error); + ICore::self()->uiController()->postMessage(message); + } } } } ProjectFileItem* createFile(const ProjectFolderItem* item) { QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); QString name = QInputDialog::getText(window, i18n("Create File in %1", item->path().pathOrUrl()), i18n("File name:")); if(name.isEmpty()) return nullptr; ProjectFileItem* ret = item->project()->projectFileManager()->addFile( Path(item->path(), name), item->folder() ); if (ret) { ICore::self()->documentController()->openDocument( ret->path().toUrl() ); } return ret; } void ProjectManagerViewPlugin::createFileFromContextMenu( ) { const auto items = itemsFromIndexes(d->ctxProjectItemList); for (KDevelop::ProjectBaseItem* item : items) { if ( item->folder() ) { createFile(item->folder()); } else if ( item->target() ) { auto* folder=dynamic_cast(item->parent()); if(folder) { ProjectFileItem* f=createFile(folder); if(f) item->project()->buildSystemManager()->addFilesToTarget(QList() << f, item->target()); } } } } void ProjectManagerViewPlugin::copyFromContextMenu() { qApp->clipboard()->setMimeData(createClipboardMimeData(false)); } void ProjectManagerViewPlugin::cutFromContextMenu() { qApp->clipboard()->setMimeData(createClipboardMimeData(true)); } static void selectItemsByPaths(ProjectManagerView* view, const Path::List& paths) { KDevelop::ProjectModel* projectModel = KDevelop::ICore::self()->projectController()->projectModel(); QList newItems; for (const Path& path : paths) { QList items = projectModel->itemsForPath(IndexedString(path.path())); newItems.append(items); for (ProjectBaseItem* item : qAsConst(items)) { view->expandItem(item->parent()); } } view->selectItems(newItems); } void ProjectManagerViewPlugin::pasteFromContextMenu() { auto* ctx = static_cast(ICore::self()->selectionController()->currentSelection()); if (ctx->items().count() != 1) { return; //do nothing if multiple or none items are selected } ProjectBaseItem* destItem = ctx->items().at(0); if (!destItem->folder()) { return; //do nothing if the target is not a directory } const QMimeData* data = qApp->clipboard()->mimeData(); qCDebug(PLUGIN_PROJECTMANAGERVIEW) << data->urls(); Path::List origPaths = toPathList(data->urls()); const bool isCut = KIO::isClipboardDataCut(data); const CutCopyPasteHelpers::SourceToDestinationMap map = CutCopyPasteHelpers::mapSourceToDestination(origPaths, destItem->folder()->path()); QVector tasks = CutCopyPasteHelpers::copyMoveItems( map.filteredPaths, destItem, isCut ? CutCopyPasteHelpers::Operation::CUT : CutCopyPasteHelpers::Operation::COPY); // Select new items in the project manager view auto* itemCtx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (itemCtx) { Path::List finalPathsList; for (const auto& task : tasks) { if (task.m_status == CutCopyPasteHelpers::TaskStatus::SUCCESS && task.m_type != CutCopyPasteHelpers::TaskType::DELETION) { finalPathsList.reserve(finalPathsList.size() + task.m_src.size()); for (const Path& src : task.m_src) { finalPathsList.append(map.finalPaths[src]); } } } selectItemsByPaths(itemCtx->view(), finalPathsList); } // If there was a single failure, display a warning dialog. const bool anyFailed = std::any_of(tasks.begin(), tasks.end(), [](const CutCopyPasteHelpers::TaskInfo& task) { return task.m_status != CutCopyPasteHelpers::TaskStatus::SUCCESS; }); if (anyFailed) { QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); showWarningDialogForFailedPaste(window, tasks); } } #include "projectmanagerviewplugin.moc" diff --git a/plugins/scratchpad/CMakeLists.txt b/plugins/scratchpad/CMakeLists.txt index 31bd368800..ba00fa6889 100644 --- a/plugins/scratchpad/CMakeLists.txt +++ b/plugins/scratchpad/CMakeLists.txt @@ -1,27 +1,28 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevscratchpad\") set(scratchpad_SRCS scratchpad.cpp scratchpadview.cpp scratchpadjob.cpp ) ki18n_wrap_ui(scratchpad_SRCS scratchpadview.ui) qt5_add_resources(scratchpad_SRCS kdevscratchpad.qrc) declare_qt_logging_category(scratchpad_SRCS TYPE PLUGIN IDENTIFIER PLUGIN_SCRATCHPAD CATEGORY_BASENAME "scratchpad" ) kdevplatform_add_plugin(kdevscratchpad JSON scratchpad.json SOURCES ${scratchpad_SRCS} ) target_link_libraries(kdevscratchpad KDev::Interfaces + KDev::Sublime KDev::Util KDev::OutputView ) diff --git a/plugins/scratchpad/scratchpadview.cpp b/plugins/scratchpad/scratchpadview.cpp index 5b30fd6a16..12ceb0437b 100644 --- a/plugins/scratchpad/scratchpadview.cpp +++ b/plugins/scratchpad/scratchpadview.cpp @@ -1,235 +1,238 @@ /* This file is part of KDevelop * * Copyright 2018 Amish K. Naidu * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "scratchpadview.h" #include "scratchpad.h" #include #include #include +#include #include +#include #include -#include #include #include #include #include #include #include #include #include #include // Use a delegate because the dataChanged signal doesn't tell us the previous name class FileRenameDelegate : public QStyledItemDelegate { Q_OBJECT public: FileRenameDelegate(QObject* parent, Scratchpad* scratchpad) : QStyledItemDelegate(parent) , m_scratchpad(scratchpad) { } void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override { const QString previousName = index.data().toString(); QStyledItemDelegate::setModelData(editor, model, index); const auto* proxyModel = static_cast(model); m_scratchpad->renameScratch(proxyModel->mapToSource(index), previousName); } private: Scratchpad* m_scratchpad; }; EmptyMessageListView::EmptyMessageListView(QWidget* parent) : QListView(parent) { } void EmptyMessageListView::paintEvent(QPaintEvent* event) { if (model() && model()->rowCount(rootIndex()) > 0) { QListView::paintEvent(event); } else { QPainter painter(viewport()); const auto margin = QMargins(parentWidget()->style()->pixelMetric(QStyle::PM_LayoutLeftMargin), 0, parentWidget()->style()->pixelMetric(QStyle::PM_LayoutRightMargin), 0); painter.drawText(rect() - margin, Qt::AlignCenter | Qt::TextWordWrap, m_message); } } void EmptyMessageListView::setEmptyMessage(const QString& message) { m_message = message; } ScratchpadView::ScratchpadView(QWidget* parent, Scratchpad* scratchpad) : QWidget(parent) , m_scratchpad(scratchpad) { setupUi(this); setupActions(); setWindowTitle(i18n("Scratchpad")); setWindowIcon(QIcon::fromTheme(QStringLiteral("note"))); auto* const modelProxy = new QSortFilterProxyModel(this); modelProxy->setSourceModel(m_scratchpad->model()); modelProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); modelProxy->setSortCaseSensitivity(Qt::CaseInsensitive); modelProxy->setSortRole(Qt::DisplayRole); connect(m_filter, &QLineEdit::textEdited, modelProxy, &QSortFilterProxyModel::setFilterWildcard); scratchView->setModel(modelProxy); scratchView->setItemDelegate(new FileRenameDelegate(this, m_scratchpad)); scratchView->setEmptyMessage(i18n("Scratchpad lets you quickly run and experiment with code without a full project, and even store todos. Create a new scratch to start.")); connect(scratchView, &QListView::activated, this, &ScratchpadView::scratchActivated); - connect(m_scratchpad, &Scratchpad::actionFailed, this, [this](const QString& message) { - KMessageBox::sorry(this, message); + connect(m_scratchpad, &Scratchpad::actionFailed, this, [this](const QString& messageText) { + // TODO: could be also messagewidget inside toolview? + auto* message = new Sublime::Message(messageText, Sublime::Message::Error); + KDevelop::ICore::self()->uiController()->postMessage(message); }); connect(commandWidget, &QLineEdit::returnPressed, this, &ScratchpadView::runSelectedScratch); connect(commandWidget, &QLineEdit::returnPressed, this, [this] { m_scratchpad->setCommand(proxyModel()->mapToSource(currentIndex()), commandWidget->text()); }); commandWidget->setToolTip(i18n("Command to run this scratch. $f will expand to the scratch path")); commandWidget->setPlaceholderText(commandWidget->toolTip()); // change active scratch when changing document connect(KDevelop::ICore::self()->documentController(), &KDevelop::IDocumentController::documentActivated, this, [this](const KDevelop::IDocument* document) { if (document->url().isLocalFile()) { const auto* model = scratchView->model(); const auto index = model->match(model->index(0, 0), Scratchpad::FullPathRole, document->url().toLocalFile()).value({}); if (index.isValid()) { scratchView->setCurrentIndex(index); } } }); connect(scratchView, &QAbstractItemView::pressed, this, &ScratchpadView::validateItemActions); validateItemActions(); } void ScratchpadView::setupActions() { QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("New Scratch"), this); connect(action, &QAction::triggered, this, &ScratchpadView::createScratch); addAction(action); action = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove Scratch"), this); connect(action, &QAction::triggered, this, [this] { m_scratchpad->removeScratch(proxyModel()->mapToSource(currentIndex())); validateItemActions(); }); addAction(action); m_itemActions.push_back(action); action = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename Scratch"), this); connect(action, &QAction::triggered, this, [this] { scratchView->edit(scratchView->currentIndex()); }); addAction(action); m_itemActions.push_back(action); action = m_scratchpad->runAction(); action->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); action->setText(i18n("Run Scratch")); connect(action, &QAction::triggered, this, &ScratchpadView::runSelectedScratch); addAction(action); m_itemActions.push_back(action); m_filter = new QLineEdit(this); m_filter->setPlaceholderText(i18n("Filter...")); auto filterAction = new QWidgetAction(this); filterAction->setDefaultWidget(m_filter); addAction(filterAction); } void ScratchpadView::validateItemActions() { bool enable = currentIndex().isValid(); for (auto* action : qAsConst(m_itemActions)) { action->setEnabled(enable); } commandWidget->setReadOnly(!enable); if (!enable) { commandWidget->clear(); } commandWidget->setText(currentIndex().data(Scratchpad::RunCommandRole).toString()); } void ScratchpadView::runSelectedScratch() { const auto sourceIndex = proxyModel()->mapToSource(currentIndex()); if (auto* document = KDevelop::ICore::self()->documentController()->documentForUrl( QUrl::fromLocalFile(sourceIndex.data(Scratchpad::FullPathRole).toString()))) { document->save(); } m_scratchpad->setCommand(sourceIndex, commandWidget->text()); m_scratchpad->runScratch(sourceIndex); } void ScratchpadView::scratchActivated(const QModelIndex& index) { validateItemActions(); m_scratchpad->openScratch(proxyModel()->mapToSource(index)); } void ScratchpadView::createScratch() { QString name = QInputDialog::getText(this, i18n("Create New Scratch"), i18n("Enter name for scratch file:"), QLineEdit::Normal, QStringLiteral("example.cpp")); if (!name.isEmpty()) { m_scratchpad->createScratch(name); } } QAbstractProxyModel* ScratchpadView::proxyModel() const { return static_cast(scratchView->model()); } QModelIndex ScratchpadView::currentIndex() const { return scratchView->currentIndex(); } #include "scratchpadview.moc" #include "moc_emptymessagelistview.cpp"