File indexing completed on 2024-04-21 15:24:34

0001 /*
0002     SPDX-FileCopyrightText: 2013 Sven Brauch <svenbrauch@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "docfilewizard.h"
0008 #include "docfilemanagerwidget.h"
0009 
0010 #include <QGroupBox>
0011 #include <QFormLayout>
0012 #include <QLabel>
0013 #include <QLineEdit>
0014 #include <QBoxLayout>
0015 #include <QPushButton>
0016 #include <QTabWidget>
0017 #include <QScrollBar>
0018 #include <QDebug>
0019 #include <QDir>
0020 #include <QStandardPaths>
0021 
0022 #include <KLocalizedString>
0023 #include <KMessageBox>
0024 #include <KProcess>
0025 #include <interfaces/icore.h>
0026 #include <interfaces/iproject.h>
0027 #include <interfaces/iprojectcontroller.h>
0028 #include <project/projectmodel.h>
0029 #include <util/path.h>
0030 
0031 DocfileWizard::DocfileWizard(const QString& workingDirectory, QWidget* parent)
0032     : QDialog(parent)
0033     , worker(nullptr)
0034     , workingDirectory(workingDirectory)
0035 {
0036     setLayout(new QVBoxLayout);
0037 
0038     // The interpreter group box
0039     QGroupBox* interpreter = new QGroupBox;
0040     interpreter->setTitle(i18n("Configure the Python interpreter to use"));
0041     QFormLayout* interpreterLayout = new QFormLayout;
0042     interpreterField = new QLineEdit("python");
0043     interpreterLayout->addRow(new QLabel(i18n("Python executable")), interpreterField);
0044     interpreter->setLayout(interpreterLayout);
0045 
0046     // The module + output file group box
0047     QGroupBox* module = new QGroupBox;
0048     module->setTitle(i18n("Select a python module to generate documentation for"));
0049     QFormLayout* moduleLayout = new QFormLayout;
0050     moduleField = new QLineEdit;
0051     moduleLayout->addRow(new QLabel(i18nc("refers to selecting a python module to perform some operation on",
0052                                           "Target module (e.g. \"math\")")), moduleField);
0053     outputFilenameField = new QLineEdit;
0054     moduleLayout->addRow(new QLabel(i18n("Output filename")), outputFilenameField);
0055     module->setLayout(moduleLayout);
0056 
0057     // Status group box
0058     QGroupBox* status = new QGroupBox;
0059     QTabWidget* tabs = new QTabWidget;
0060     status->setTitle(i18n("Status and output"));
0061     statusField = new QTextEdit();
0062     statusField->setText(i18n("The process has not been run yet."));
0063     statusField->setFontFamily("monospace");
0064     statusField->setLineWrapMode(QTextEdit::NoWrap);
0065     statusField->setReadOnly(true);
0066     statusField->setAcceptRichText(false);
0067     resultField = new QTextEdit();
0068     resultField->setText(i18n("The process has not been run yet."));
0069     resultField->setFontFamily("monospace");
0070     resultField->setLineWrapMode(QTextEdit::NoWrap);
0071     resultField->setReadOnly(true);
0072     resultField->setAcceptRichText(false);
0073     status->setLayout(new QHBoxLayout);
0074     tabs->addTab(statusField, i18n("Script output"));
0075     tabs->addTab(resultField, i18n("Results"));
0076     status->layout()->addWidget(tabs);
0077 
0078     // The run / close buttons
0079     QHBoxLayout* buttonsLayout = new QHBoxLayout;
0080     buttonsLayout->setDirection(QBoxLayout::RightToLeft);
0081     QPushButton* closeButton = new QPushButton(i18n("Close"));
0082     closeButton->setIcon(QIcon::fromTheme("dialog-close"));
0083     saveButton = new QPushButton(i18n("Save and close"));
0084     saveButton->setEnabled(false);
0085     saveButton->setIcon(QIcon::fromTheme("dialog-ok-apply"));
0086     runButton = new QPushButton(i18n("Generate"));
0087     runButton->setDefault(true);
0088     runButton->setIcon(QIcon::fromTheme("tools-wizard"));
0089     buttonsLayout->addWidget(closeButton);
0090     buttonsLayout->addWidget(runButton);
0091     buttonsLayout->addWidget(saveButton);
0092     buttonsLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding));
0093 
0094     // connections
0095     QObject::connect(closeButton, &QAbstractButton::clicked, this, &QWidget::close);
0096     QObject::connect(saveButton, &QAbstractButton::clicked, this, &DocfileWizard::saveAndClose);
0097     QObject::connect(moduleField, &QLineEdit::textChanged, this, &DocfileWizard::updateOutputFilename);
0098     QObject::connect(runButton, &QAbstractButton::clicked, this, &DocfileWizard::run);
0099 
0100     // putting it all together
0101     layout()->addWidget(interpreter);
0102     layout()->addWidget(module);
0103     layout()->addWidget(status);
0104     layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding));
0105     qobject_cast<QVBoxLayout*>(layout())->addLayout(buttonsLayout); // TODO ugh
0106 
0107     resize(640, 480);
0108 }
0109 
0110 const QString DocfileWizard::wasSavedAs() const
0111 {
0112     return savedAs;
0113 }
0114 
0115 QString DocfileWizard::fileNameForModule(QString moduleName) const
0116 {
0117     if ( moduleName.isEmpty() ) {
0118         return moduleName;
0119     }
0120     return moduleName.replace('.', '/') + ".py";
0121 }
0122 
0123 void DocfileWizard::setModuleName(const QString& moduleName)
0124 {
0125     moduleField->setText(moduleName);
0126 }
0127 
0128 bool DocfileWizard::run()
0129 {
0130     // validate input data, setup and program state
0131     if ( worker ) {
0132         // process already running
0133         return false;
0134     }
0135     QString scriptUrl = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kdevpythonsupport/scripts/introspect.py");
0136     if ( scriptUrl.isEmpty() ) {
0137         KMessageBox::error(this, i18n("Couldn't find the introspect.py script; check your installation!"));
0138         return false;
0139     }
0140     if ( workingDirectory.isEmpty() ) {
0141         KMessageBox::error(this, i18n("Couldn't find a valid kdev-python data directory; check your installation!"));
0142         return false;
0143     }
0144     QString outputFilename = outputFilenameField->text();
0145     if ( outputFilename.contains("..") ) {
0146         // protect the user from writing outside the data directory accidentally
0147         KMessageBox::error(this, i18n("Invalid output filename"));
0148         return false;
0149     }
0150 
0151     runButton->setEnabled(false);
0152 
0153     // clean output from previous script runs; since the fields are set to readonly,
0154     // no user data will be lost
0155     statusField->clear();
0156     resultField->clear();
0157 
0158     // set up the process and connect relevant slots
0159     QString interpreter = interpreterField->text();
0160     QString module = moduleField->text();
0161     worker = new QProcess(this);
0162     QObject::connect(worker, &QProcess::readyReadStandardError, this, &DocfileWizard::processScriptOutput);
0163     QObject::connect(worker, &QProcess::readyReadStandardOutput, this, &DocfileWizard::processScriptOutput);
0164     QObject::connect(worker, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &DocfileWizard::processFinished);
0165 
0166     // can never have too many slashes
0167     outputFile.setFileName(workingDirectory + "/" + outputFilename);
0168     
0169     QList<KDevelop::IProject*> projs = KDevelop::ICore::self()->projectController()->projects();
0170     QStringList args;
0171     args << scriptUrl;
0172     foreach(const KDevelop::IProject* proj, projs)
0173     {
0174         if ( proj )
0175             args << proj->path().toLocalFile();
0176     }
0177     args << module;
0178     worker->start(interpreter, args);
0179     return true;
0180 }
0181 
0182 void DocfileWizard::saveAndClose()
0183 {
0184     bool mayWrite = true;
0185     if ( outputFile.exists() ) {
0186         mayWrite = KMessageBox::questionYesNo(this, i18n("The output file <br/>%1<br/> already exists, "
0187                                                          "do you want to overwrite it?",
0188                                                           outputFile.fileName())) == KMessageBox::Yes;
0189     }
0190     if ( mayWrite ) {
0191         auto url = QUrl::fromLocalFile(outputFile.fileName());
0192         Q_ASSERT(url.isLocalFile());
0193         auto basePath = url.url(QUrl::RemoveFilename | QUrl::PreferLocalFile);
0194 
0195         // should have been done previously
0196         Q_ASSERT(QDir(basePath).exists());
0197         if ( ! QDir(basePath).exists() ) {
0198             QDir(basePath).mkpath(basePath);
0199         }
0200         outputFile.open(QIODevice::WriteOnly);
0201         QString header = "\"\"\"" + i18n("This file contains auto-generated documentation extracted\n"
0202                                          "from python run-time information. It is analyzed by KDevelop\n"
0203                                          "to offer features such as code-completion and syntax highlighting.\n"
0204                                          "If you discover errors in KDevelop's support for this module,\n"
0205                                          "you can edit this file to correct the errors, e.g. you can add\n"
0206                                          "additional return statements to functions to control the return\n"
0207                                          "type to be used for that function by the analyzer.\n"
0208                                          "Make sure to keep a copy of your changes so you don't accidentally\n"
0209                                          "overwrite them by re-generating the file.\n") + "\"\"\"\n\n";
0210         outputFile.write(header.toUtf8() + resultField->toPlainText().toUtf8());
0211         outputFile.close();
0212         savedAs = outputFile.fileName();
0213         close();
0214     }
0215 }
0216 
0217 void DocfileWizard::processScriptOutput()
0218 {
0219     statusField->insertPlainText(worker->readAllStandardError());
0220     resultField->insertPlainText(worker->readAllStandardOutput());
0221     QScrollBar* scrollbar = statusField->verticalScrollBar();
0222     scrollbar->setValue(scrollbar->maximum());
0223 }
0224 
0225 void DocfileWizard::processFinished(int, QProcess::ExitStatus)
0226 {
0227     worker = nullptr;
0228     runButton->setEnabled(true);
0229     saveButton->setEnabled(true);
0230 }
0231 
0232 void DocfileWizard::updateOutputFilename(const QString& newModuleName)
0233 {
0234     QString newFileName = fileNameForModule(newModuleName);
0235     if ( fileNameForModule(previousModuleName) == outputFilenameField->text() ) {
0236         // the user didn't edit the field, or edited it to what it is anyways, so update the text
0237         // otherwise, do nothing.
0238         outputFilenameField->setText(newFileName);
0239     }
0240     previousModuleName = newModuleName;
0241 }
0242 
0243 #include "moc_docfilewizard.cpp"