// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "externaleditors.h" #include "qmakeprojectmanagertr.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ProjectExplorer; using namespace Utils; enum { debug = 0 }; namespace QmakeProjectManager { namespace Internal { // ------------ Messages static inline QString msgStartFailed(const QString &binary, QStringList arguments) { arguments.push_front(binary); return Tr::tr("Unable to start \"%1\"").arg(arguments.join(QLatin1Char(' '))); } static inline QString msgAppNotFound(const QString &id) { return Tr::tr("The application \"%1\" could not be found.").arg(id); } // -- Commands and helpers static QString linguistBinary(const QtSupport::QtVersion *qtVersion) { if (qtVersion) return qtVersion->linguistFilePath().toString(); return QLatin1String(HostOsInfo::isMacHost() ? "Linguist" : "linguist"); } static QString designerBinary(const QtSupport::QtVersion *qtVersion) { if (qtVersion) return qtVersion->designerFilePath().toString(); return QLatin1String(HostOsInfo::isMacHost() ? "Designer" : "designer"); } // Mac: Change the call 'Foo.app/Contents/MacOS/Foo ' to // 'open -a Foo.app '. doesn't support generic command line arguments static ExternalQtEditor::LaunchData createMacOpenCommand(const ExternalQtEditor::LaunchData &data) { ExternalQtEditor::LaunchData openData = data; const int appFolderIndex = data.binary.lastIndexOf(QLatin1String("/Contents/MacOS/")); if (appFolderIndex != -1) { openData.binary = "open"; openData.arguments = QStringList({QString("-a"), data.binary.left(appFolderIndex)}) + data.arguments; } return openData; } static const char designerIdC[] = "Qt.Designer"; static const char linguistIdC[] = "Qt.Linguist"; static const char designerDisplayName[] = QT_TRANSLATE_NOOP("OpenWith::Editors", "Qt Designer"); static const char linguistDisplayName[] = QT_TRANSLATE_NOOP("OpenWith::Editors", "Qt Linguist"); // -------------- ExternalQtEditor ExternalQtEditor::ExternalQtEditor(Id id, const QString &displayName, const QString &mimetype, const CommandForQtVersion &commandForQtVersion) : m_commandForQtVersion(commandForQtVersion) { setId(id); setDisplayName(displayName); setMimeTypes({mimetype}); } ExternalQtEditor *ExternalQtEditor::createLinguistEditor() { return new ExternalQtEditor(linguistIdC, QLatin1String(linguistDisplayName), QLatin1String(ProjectExplorer::Constants::LINGUIST_MIMETYPE), linguistBinary); } ExternalQtEditor *ExternalQtEditor::createDesignerEditor() { if (HostOsInfo::isMacHost()) { return new ExternalQtEditor(designerIdC, QLatin1String(designerDisplayName), QLatin1String(ProjectExplorer::Constants::FORM_MIMETYPE), designerBinary); } else { return new DesignerExternalEditor; } } static QString findFirstCommand(const QVector &qtVersions, ExternalQtEditor::CommandForQtVersion command) { for (QtSupport::QtVersion *qt : qtVersions) { if (qt) { const QString binary = command(qt); if (!binary.isEmpty()) return binary; } } return QString(); } bool ExternalQtEditor::getEditorLaunchData(const FilePath &filePath, LaunchData *data, QString *errorMessage) const { // Check in order for Qt version with the binary: // - active kit of project // - any other of the project // - default kit // - any other kit // As fallback check PATH data->workingDirectory.clear(); QVector qtVersionsToCheck; // deduplicated after being filled if (const Project *project = SessionManager::projectForFile(filePath)) { data->workingDirectory = project->projectDirectory(); // active kit if (const Target *target = project->activeTarget()) { qtVersionsToCheck << QtSupport::QtKitAspect::qtVersion(target->kit()); } // all kits of project qtVersionsToCheck += Utils::transform(project->targets(), [](Target *t) { return QTC_GUARD(t) ? QtSupport::QtKitAspect::qtVersion(t->kit()) : nullptr; }); } // default kit qtVersionsToCheck << QtSupport::QtKitAspect::qtVersion(KitManager::defaultKit()); // all kits qtVersionsToCheck += Utils::transform(KitManager::kits(), QtSupport::QtKitAspect::qtVersion); qtVersionsToCheck = Utils::filteredUnique(qtVersionsToCheck); // can still contain nullptr data->binary = findFirstCommand(qtVersionsToCheck, m_commandForQtVersion); // fallback if (data->binary.isEmpty()) { const QString path = qtcEnvironmentVariable("PATH"); data->binary = QtcProcess::locateBinary(path, m_commandForQtVersion(nullptr)); } if (data->binary.isEmpty()) { *errorMessage = msgAppNotFound(id().toString()); return false; } // Setup binary + arguments, use Mac Open if appropriate data->arguments.push_back(filePath.toString()); if (HostOsInfo::isMacHost()) *data = createMacOpenCommand(*data); if (debug) qDebug() << Q_FUNC_INFO << '\n' << data->binary << data->arguments; return true; } bool ExternalQtEditor::startEditor(const FilePath &filePath, QString *errorMessage) { LaunchData data; return getEditorLaunchData(filePath, &data, errorMessage) && startEditorProcess(data, errorMessage); } bool ExternalQtEditor::startEditorProcess(const LaunchData &data, QString *errorMessage) { if (debug) qDebug() << Q_FUNC_INFO << '\n' << data.binary << data.arguments << data.workingDirectory; qint64 pid = 0; if (!QtcProcess::startDetached({FilePath::fromString(data.binary), data.arguments}, data.workingDirectory, &pid)) { *errorMessage = msgStartFailed(data.binary, data.arguments); return false; } return true; } // --------------- DesignerExternalEditor with Designer Tcp remote control. DesignerExternalEditor::DesignerExternalEditor() : ExternalQtEditor(designerIdC, QLatin1String(designerDisplayName), QLatin1String(Designer::Constants::FORM_MIMETYPE), designerBinary) { } void DesignerExternalEditor::processTerminated(const QString &binary) { const ProcessCache::iterator it = m_processCache.find(binary); if (it == m_processCache.end()) return; // Make sure socket is closed and cleaned, remove from cache QTcpSocket *socket = it.value(); m_processCache.erase(it); // Note that closing will cause the slot to be retriggered if (debug) qDebug() << Q_FUNC_INFO << '\n' << binary << socket->state(); if (socket->state() == QAbstractSocket::ConnectedState) socket->close(); socket->deleteLater(); } bool DesignerExternalEditor::startEditor(const FilePath &filePath, QString *errorMessage) { LaunchData data; // Find the editor binary if (!getEditorLaunchData(filePath, &data, errorMessage)) { return false; } // Known one? const ProcessCache::iterator it = m_processCache.find(data.binary); if (it != m_processCache.end()) { // Process is known, write to its socket to cause it to open the file if (debug) qDebug() << Q_FUNC_INFO << "\nWriting to socket:" << data.binary << filePath; QTcpSocket *socket = it.value(); if (!socket->write(filePath.toString().toUtf8() + '\n')) { *errorMessage = Tr::tr("Qt Designer is not responding (%1).").arg(socket->errorString()); return false; } return true; } // No process yet. Create socket & launch the process QTcpServer server; if (!server.listen(QHostAddress::LocalHost)) { *errorMessage = Tr::tr("Unable to create server socket: %1").arg(server.errorString()); return false; } const quint16 port = server.serverPort(); if (debug) qDebug() << Q_FUNC_INFO << "\nLaunching server:" << port << data.binary << filePath; // Start first one with file and socket as '-client port file' // Wait for the socket listening data.arguments.push_front(QString::number(port)); data.arguments.push_front(QLatin1String("-client")); if (!startEditorProcess(data, errorMessage)) return false; // Insert into cache if socket is created, else try again next time if (server.waitForNewConnection(3000)) { QTcpSocket *socket = server.nextPendingConnection(); socket->setParent(this); const QString binary = data.binary; m_processCache.insert(binary, socket); auto mapSlot = [this, binary] { processTerminated(binary); }; connect(socket, &QAbstractSocket::disconnected, this, mapSlot); connect(socket, &QAbstractSocket::errorOccurred, this, mapSlot); } return true; } } // namespace Internal } // namespace QmakeProjectManager