diff --git a/src/plugins/coreplugin/icore.cpp b/src/plugins/coreplugin/icore.cpp index 2ab0936300a..b5986bd67c6 100644 --- a/src/plugins/coreplugin/icore.cpp +++ b/src/plugins/coreplugin/icore.cpp @@ -356,8 +356,26 @@ QString ICore::cacheResourcePath() QString ICore::installerResourcePath() { - return QFileInfo(settings(QSettings::SystemScope)->fileName()).path() + '/' - + Constants::IDE_ID; + return QFileInfo(settings(QSettings::SystemScope)->fileName()).path() + '/' + Constants::IDE_ID; +} + +QString ICore::pluginPath() +{ + return QDir::cleanPath(QCoreApplication::applicationDirPath() + '/' + RELATIVE_PLUGIN_PATH); +} + +QString ICore::userPluginPath() +{ + QString pluginPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + if (Utils::HostOsInfo::isAnyUnixHost() && !Utils::HostOsInfo::isMacHost()) + pluginPath += "/data"; + pluginPath += '/' + QLatin1String(Core::Constants::IDE_SETTINGSVARIANT_STR) + '/'; + pluginPath += QLatin1String(Utils::HostOsInfo::isMacHost() ? Core::Constants::IDE_DISPLAY_NAME + : Core::Constants::IDE_ID); + pluginPath += "/plugins/"; + pluginPath += QString::number(IDE_VERSION_MAJOR) + '.' + QString::number(IDE_VERSION_MINOR) + + '.' + QString::number(IDE_VERSION_RELEASE); + return pluginPath; } /*! diff --git a/src/plugins/coreplugin/icore.h b/src/plugins/coreplugin/icore.h index 487a9715792..8610023e6a6 100644 --- a/src/plugins/coreplugin/icore.h +++ b/src/plugins/coreplugin/icore.h @@ -97,6 +97,8 @@ public: static QString userResourcePath(); static QString cacheResourcePath(); static QString installerResourcePath(); + static QString pluginPath(); + static QString userPluginPath(); static QString libexecPath(); static QString clangExecutable(const QString &clangBinDirectory); static QString clangTidyExecutable(const QString &clangBinDirectory); diff --git a/src/plugins/coreplugin/plugindialog.cpp b/src/plugins/coreplugin/plugindialog.cpp index f3dceb73a8e..466850863b9 100644 --- a/src/plugins/coreplugin/plugindialog.cpp +++ b/src/plugins/coreplugin/plugindialog.cpp @@ -29,28 +29,237 @@ #include "dialogs/restartdialog.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 + +using namespace Utils; namespace Core { namespace Internal { static bool s_isRestartRequired = false; +const char kPath[] = "Path"; +const char kApplicationInstall[] = "ApplicationInstall"; + +static bool hasLibSuffix(const QString &path) +{ + return HostOsInfo().isWindowsHost() && path.endsWith(".dll", Qt::CaseInsensitive) + || HostOsInfo().isLinuxHost() && QFileInfo(path).completeSuffix().startsWith(".so") + || HostOsInfo().isMacHost() && path.endsWith(".dylib"); +} + +static bool isZipFile(const QString &path) +{ + const QList mimeType = mimeTypesForFileName(path); + return anyOf(mimeType, [](const MimeType &mt) { return mt.inherits("application/zip"); }); +} + +struct Tool +{ + FilePath executable; + QStringList arguments; +}; + +static Utils::optional unzipTool(const FilePath &src, const FilePath &dest) +{ + const FilePath unzip = Utils::Environment::systemEnvironment().searchInPath( + Utils::HostOsInfo::withExecutableSuffix("unzip")); + if (!unzip.isEmpty()) + return Tool{unzip, {"-o", src.toString(), "-d", dest.toString()}}; + + const FilePath sevenzip = Utils::Environment::systemEnvironment().searchInPath( + Utils::HostOsInfo::withExecutableSuffix("7z")); + if (!sevenzip.isEmpty()) + return Tool{sevenzip, {"x", QString("-o") + dest.toString(), "-y", src.toString()}}; + return Utils::nullopt; + + const FilePath cmake = Utils::Environment::systemEnvironment().searchInPath( + Utils::HostOsInfo::withExecutableSuffix("cmake")); + if (!cmake.isEmpty()) + return Tool{cmake, {"-E", "tar", "xvf", src.toString()}}; +} + +class SourcePage : public WizardPage +{ +public: + SourcePage(QWidget *parent) + : WizardPage(parent) + { + setTitle(PluginDialog::tr("Source")); + auto vlayout = new QVBoxLayout; + setLayout(vlayout); + + auto label = new QLabel( + "

" + + PluginDialog::tr( + "Choose source location. This can be a plugin library file or a zip file.") + + "

"); + label->setWordWrap(true); + vlayout->addWidget(label); + + auto path = new PathChooser; + path->setExpectedKind(PathChooser::Any); + vlayout->addWidget(path); + registerFieldWithName(kPath, path, "path", SIGNAL(pathChanged(QString))); + connect(path, &PathChooser::pathChanged, this, &SourcePage::updateWarnings); + + m_info = new InfoLabel; + m_info->setType(InfoLabel::Error); + m_info->setVisible(false); + vlayout->addWidget(m_info); + } + + void updateWarnings() + { + m_info->setVisible(!isComplete()); + emit completeChanged(); + } + + bool isComplete() const + { + const QString path = field(kPath).toString(); + if (!QFile::exists(path)) { + m_info->setText(PluginDialog::tr("File does not exist.")); + return false; + } + if (hasLibSuffix(path)) + return true; + + if (!isZipFile(path)) { + m_info->setText(PluginDialog::tr("File format not supported.")); + return false; + } + if (!unzipTool({}, {})) { + m_info->setText( + PluginDialog::tr("Could not find unzip, 7z, or cmake executable in PATH.")); + return false; + } + return true; + } + + InfoLabel *m_info = nullptr; +}; + +class InstallLocationPage : public WizardPage +{ +public: + InstallLocationPage(QWidget *parent) + : WizardPage(parent) + { + setTitle(PluginDialog::tr("Install Location")); + auto vlayout = new QVBoxLayout; + setLayout(vlayout); + + auto label = new QLabel("

" + PluginDialog::tr("Choose install location.") + "

"); + label->setWordWrap(true); + vlayout->addWidget(label); + vlayout->addSpacing(10); + + auto localInstall = new QRadioButton(PluginDialog::tr("User plugins")); + localInstall->setChecked(true); + auto localLabel = new QLabel( + PluginDialog::tr("The plugin will be available to all compatible %1 " + "installations, but only for the current user.") + .arg(Constants::IDE_DISPLAY_NAME)); + localLabel->setWordWrap(true); + localLabel->setAttribute(Qt::WA_MacSmallSize, true); + + vlayout->addWidget(localInstall); + vlayout->addWidget(localLabel); + vlayout->addSpacing(10); + + auto appInstall = new QRadioButton( + PluginDialog::tr("%1 installation").arg(Constants::IDE_DISPLAY_NAME)); + auto appLabel = new QLabel( + PluginDialog::tr("The plugin will be available only to this %1 " + "installation, but for all users that can access it.") + .arg(Constants::IDE_DISPLAY_NAME)); + appLabel->setWordWrap(true); + appLabel->setAttribute(Qt::WA_MacSmallSize, true); + vlayout->addWidget(appInstall); + vlayout->addWidget(appLabel); + + auto group = new QButtonGroup(this); + group->addButton(localInstall); + group->addButton(appInstall); + + registerFieldWithName(kApplicationInstall, this); + setField(kApplicationInstall, false); + connect(appInstall, &QRadioButton::toggled, this, [this](bool toggled) { + setField(kApplicationInstall, toggled); + }); + } +}; + +static FilePath pluginInstallPath(QWizard *wizard) +{ + return FilePath::fromString(wizard->field(kApplicationInstall).toBool() + ? ICore::pluginPath() + : ICore::userPluginPath()); +} + +static FilePath pluginFilePath(QWizard *wizard) +{ + return FilePath::fromVariant(wizard->field(kPath)); +} + +class SummaryPage : public WizardPage +{ +public: + SummaryPage(QWidget *parent) + : WizardPage(parent) + { + setTitle(PluginDialog::tr("Summary")); + + auto vlayout = new QVBoxLayout; + setLayout(vlayout); + + m_summaryLabel = new QLabel(this); + m_summaryLabel->setWordWrap(true); + vlayout->addWidget(m_summaryLabel); + } + + void initializePage() + { + m_summaryLabel->setText(PluginDialog::tr("\"%1\" will be installed into \"%2\".") + .arg(pluginFilePath(wizard()).toUserOutput(), + pluginInstallPath(wizard()).toUserOutput())); + } + +private: + QLabel *m_summaryLabel; +}; + PluginDialog::PluginDialog(QWidget *parent) : QDialog(parent), m_view(new ExtensionSystem::PluginView(this)) @@ -78,6 +287,7 @@ PluginDialog::PluginDialog(QWidget *parent) m_detailsButton = new QPushButton(tr("Details"), this); m_errorDetailsButton = new QPushButton(tr("Error Details"), this); m_closeButton = new QPushButton(tr("Close"), this); + m_installButton = new QPushButton(tr("Install Plugin..."), this); m_detailsButton->setEnabled(false); m_errorDetailsButton->setEnabled(false); m_closeButton->setEnabled(true); @@ -90,6 +300,7 @@ PluginDialog::PluginDialog(QWidget *parent) auto hl = new QHBoxLayout; hl->addWidget(m_detailsButton); hl->addWidget(m_errorDetailsButton); + hl->addWidget(m_installButton); hl->addSpacing(10); hl->addWidget(m_restartRequired); hl->addStretch(5); @@ -110,8 +321,8 @@ PluginDialog::PluginDialog(QWidget *parent) [this] { openDetails(m_view->currentPlugin()); }); connect(m_errorDetailsButton, &QAbstractButton::clicked, this, &PluginDialog::openErrorDetails); - connect(m_closeButton, &QAbstractButton::clicked, - this, &PluginDialog::closeDialog); + connect(m_installButton, &QAbstractButton::clicked, this, &PluginDialog::showInstallWizard); + connect(m_closeButton, &QAbstractButton::clicked, this, &PluginDialog::closeDialog); updateButtons(); } @@ -126,6 +337,99 @@ void PluginDialog::closeDialog() accept(); } +static bool copyPluginFile(const FilePath &src, const FilePath &dest) +{ + const FilePath destFile = dest.pathAppended(src.fileName()); + if (QFile::exists(destFile.toString())) { + QMessageBox box(QMessageBox::Question, + PluginDialog::tr("Overwrite File"), + PluginDialog::tr("The file \"%1\" exists. Overwrite?") + .arg(destFile.toUserOutput()), + QMessageBox::Cancel, + ICore::dialogParent()); + QPushButton *acceptButton = box.addButton(PluginDialog::tr("Overwrite"), QMessageBox::AcceptRole); + box.setDefaultButton(acceptButton); + box.exec(); + if (box.clickedButton() != acceptButton) + return false; + QFile::remove(destFile.toString()); + } + QDir(dest.toString()).mkpath("."); + if (!QFile::copy(src.toString(), destFile.toString())) { + QMessageBox::warning(ICore::dialogParent(), + PluginDialog::tr("Failed to Write File"), + PluginDialog::tr("Failed to write file \"%1\".") + .arg(destFile.toUserOutput())); + return false; + } + return true; +} + +static bool unzip(const FilePath &src, const FilePath &dest) +{ + const Utils::optional tool = unzipTool(src, dest); + QTC_ASSERT(tool, return false); + const QString workingDirectory = dest.toFileInfo().absoluteFilePath(); + QDir(workingDirectory).mkpath("."); + QMessageBox box(QMessageBox::Information, + PluginDialog::tr("Unzipping File"), + PluginDialog::tr("Unzipping \"%1\" to \"%2\".") + .arg(src.toUserOutput(), dest.toUserOutput()), + QMessageBox::Ok | QMessageBox::Cancel, + ICore::dialogParent()); + box.button(QMessageBox::Ok)->setEnabled(false); + box.setDetailedText( + PluginDialog::tr("Running %1\nin \"%2\".\n\n", "Running in ") + .arg(CommandLine(tool->executable, tool->arguments).toUserOutput(), workingDirectory)); + QProcess process; + process.setProcessChannelMode(QProcess::MergedChannels); + QObject::connect(&process, &QProcess::readyReadStandardOutput, &box, [&box, &process]() { + box.setDetailedText(box.detailedText() + QString::fromUtf8(process.readAllStandardOutput())); + }); + QObject::connect(&process, + QOverload::of(&QProcess::finished), + [&box](int, QProcess::ExitStatus) { + box.button(QMessageBox::Ok)->setEnabled(true); + box.button(QMessageBox::Cancel)->setEnabled(false); + }); + QObject::connect(&box, &QMessageBox::rejected, &process, [&process] { + SynchronousProcess::stopProcess(process); + }); + process.setProgram(tool->executable.toString()); + process.setArguments(tool->arguments); + process.setWorkingDirectory(workingDirectory); + process.start(QProcess::ReadOnly); + box.exec(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +void PluginDialog::showInstallWizard() +{ + Wizard wizard(ICore::dialogParent()); + wizard.setWindowTitle(tr("Install Plugin")); + + auto filePage = new SourcePage(&wizard); + wizard.addPage(filePage); + + auto installLocationPage = new InstallLocationPage(&wizard); + wizard.addPage(installLocationPage); + + auto summaryPage = new SummaryPage(&wizard); + wizard.addPage(summaryPage); + + if (wizard.exec()) { + const FilePath path = pluginFilePath(&wizard); + const FilePath installPath = pluginInstallPath(&wizard); + if (hasLibSuffix(path.toString())) { + if (copyPluginFile(path, installPath)) + updateRestartRequired(); + } else if (isZipFile(path.toString())) { + if (unzip(path, installPath)) + updateRestartRequired(); + } + } +} + void PluginDialog::updateRestartRequired() { // just display the notice all the time after once changing something diff --git a/src/plugins/coreplugin/plugindialog.h b/src/plugins/coreplugin/plugindialog.h index 44e84637ddc..7c237d8a1f1 100644 --- a/src/plugins/coreplugin/plugindialog.h +++ b/src/plugins/coreplugin/plugindialog.h @@ -53,11 +53,13 @@ private: void openDetails(ExtensionSystem::PluginSpec *spec); void openErrorDetails(); void closeDialog(); + void showInstallWizard(); ExtensionSystem::PluginView *m_view; QPushButton *m_detailsButton; QPushButton *m_errorDetailsButton; + QPushButton *m_installButton; QPushButton *m_closeButton; QLabel *m_restartRequired; };