1 /*
2     SPDX-FileCopyrightText: 2013 Sven Brauch <svenbrauch@gmail.com>
3 
4     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6 
7 #include "docfilewizard.h"
8 #include "docfilemanagerwidget.h"
9 
10 #include <QGroupBox>
11 #include <QFormLayout>
12 #include <QLabel>
13 #include <QLineEdit>
14 #include <QBoxLayout>
15 #include <QPushButton>
16 #include <QTabWidget>
17 #include <QScrollBar>
18 #include <QDebug>
19 #include <QDir>
20 #include <QStandardPaths>
21 
22 #include <KLocalizedString>
23 #include <KMessageBox>
24 #include <KProcess>
25 #include <interfaces/icore.h>
26 #include <interfaces/iproject.h>
27 #include <interfaces/iprojectcontroller.h>
28 #include <project/projectmodel.h>
29 #include <util/path.h>
30 
DocfileWizard(const QString & workingDirectory,QWidget * parent)31 DocfileWizard::DocfileWizard(const QString& workingDirectory, QWidget* parent)
32     : QDialog(parent)
33     , worker(nullptr)
34     , workingDirectory(workingDirectory)
35 {
36     setLayout(new QVBoxLayout);
37 
38     // The interpreter group box
39     QGroupBox* interpreter = new QGroupBox;
40     interpreter->setTitle(i18n("Configure the Python interpreter to use"));
41     QFormLayout* interpreterLayout = new QFormLayout;
42     interpreterField = new QLineEdit("python");
43     interpreterLayout->addRow(new QLabel(i18n("Python executable")), interpreterField);
44     interpreter->setLayout(interpreterLayout);
45 
46     // The module + output file group box
47     QGroupBox* module = new QGroupBox;
48     module->setTitle(i18n("Select a python module to generate documentation for"));
49     QFormLayout* moduleLayout = new QFormLayout;
50     moduleField = new QLineEdit;
51     moduleLayout->addRow(new QLabel(i18nc("refers to selecting a python module to perform some operation on",
52                                           "Target module (e.g. \"math\")")), moduleField);
53     outputFilenameField = new QLineEdit;
54     moduleLayout->addRow(new QLabel(i18n("Output filename")), outputFilenameField);
55     module->setLayout(moduleLayout);
56 
57     // Status group box
58     QGroupBox* status = new QGroupBox;
59     QTabWidget* tabs = new QTabWidget;
60     status->setTitle(i18n("Status and output"));
61     statusField = new QTextEdit();
62     statusField->setText(i18n("The process has not been run yet."));
63     statusField->setFontFamily("monospace");
64     statusField->setLineWrapMode(QTextEdit::NoWrap);
65     statusField->setReadOnly(true);
66     statusField->setAcceptRichText(false);
67     resultField = new QTextEdit();
68     resultField->setText(i18n("The process has not been run yet."));
69     resultField->setFontFamily("monospace");
70     resultField->setLineWrapMode(QTextEdit::NoWrap);
71     resultField->setReadOnly(true);
72     resultField->setAcceptRichText(false);
73     status->setLayout(new QHBoxLayout);
74     tabs->addTab(statusField, i18n("Script output"));
75     tabs->addTab(resultField, i18n("Results"));
76     status->layout()->addWidget(tabs);
77 
78     // The run / close buttons
79     QHBoxLayout* buttonsLayout = new QHBoxLayout;
80     buttonsLayout->setDirection(QBoxLayout::RightToLeft);
81     QPushButton* closeButton = new QPushButton(i18n("Close"));
82     closeButton->setIcon(QIcon::fromTheme("dialog-close"));
83     saveButton = new QPushButton(i18n("Save and close"));
84     saveButton->setEnabled(false);
85     saveButton->setIcon(QIcon::fromTheme("dialog-ok-apply"));
86     runButton = new QPushButton(i18n("Generate"));
87     runButton->setDefault(true);
88     runButton->setIcon(QIcon::fromTheme("tools-wizard"));
89     buttonsLayout->addWidget(closeButton);
90     buttonsLayout->addWidget(runButton);
91     buttonsLayout->addWidget(saveButton);
92     buttonsLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding));
93 
94     // connections
95     QObject::connect(closeButton, &QAbstractButton::clicked, this, &QWidget::close);
96     QObject::connect(saveButton, &QAbstractButton::clicked, this, &DocfileWizard::saveAndClose);
97     QObject::connect(moduleField, &QLineEdit::textChanged, this, &DocfileWizard::updateOutputFilename);
98     QObject::connect(runButton, &QAbstractButton::clicked, this, &DocfileWizard::run);
99 
100     // putting it all together
101     layout()->addWidget(interpreter);
102     layout()->addWidget(module);
103     layout()->addWidget(status);
104     layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding));
105     qobject_cast<QVBoxLayout*>(layout())->addLayout(buttonsLayout); // TODO ugh
106 
107     resize(640, 480);
108 }
109 
wasSavedAs() const110 const QString DocfileWizard::wasSavedAs() const
111 {
112     return savedAs;
113 }
114 
fileNameForModule(QString moduleName) const115 QString DocfileWizard::fileNameForModule(QString moduleName) const
116 {
117     if ( moduleName.isEmpty() ) {
118         return moduleName;
119     }
120     return moduleName.replace('.', '/') + ".py";
121 }
122 
setModuleName(const QString & moduleName)123 void DocfileWizard::setModuleName(const QString& moduleName)
124 {
125     moduleField->setText(moduleName);
126 }
127 
run()128 bool DocfileWizard::run()
129 {
130     // validate input data, setup and program state
131     if ( worker ) {
132         // process already running
133         return false;
134     }
135     QString scriptUrl = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kdevpythonsupport/scripts/introspect.py");
136     if ( scriptUrl.isEmpty() ) {
137         KMessageBox::error(this, i18n("Couldn't find the introspect.py script; check your installation!"));
138         return false;
139     }
140     if ( workingDirectory.isEmpty() ) {
141         KMessageBox::error(this, i18n("Couldn't find a valid kdev-python data directory; check your installation!"));
142         return false;
143     }
144     QString outputFilename = outputFilenameField->text();
145     if ( outputFilename.contains("..") ) {
146         // protect the user from writing outside the data directory accidentally
147         KMessageBox::error(this, i18n("Invalid output filename"));
148         return false;
149     }
150 
151     runButton->setEnabled(false);
152 
153     // clean output from previous script runs; since the fields are set to readonly,
154     // no user data will be lost
155     statusField->clear();
156     resultField->clear();
157 
158     // set up the process and connect relevant slots
159     QString interpreter = interpreterField->text();
160     QString module = moduleField->text();
161     worker = new QProcess(this);
162     QObject::connect(worker, &QProcess::readyReadStandardError, this, &DocfileWizard::processScriptOutput);
163     QObject::connect(worker, &QProcess::readyReadStandardOutput, this, &DocfileWizard::processScriptOutput);
164     QObject::connect(worker, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &DocfileWizard::processFinished);
165 
166     // can never have too many slashes
167     outputFile.setFileName(workingDirectory + "/" + outputFilename);
168 
169     QList<KDevelop::IProject*> projs = KDevelop::ICore::self()->projectController()->projects();
170     QStringList args;
171     args << scriptUrl;
172     foreach(const KDevelop::IProject* proj, projs)
173     {
174         if ( proj )
175             args << proj->path().toLocalFile();
176     }
177     args << module;
178     worker->start(interpreter, args);
179     return true;
180 }
181 
saveAndClose()182 void DocfileWizard::saveAndClose()
183 {
184     bool mayWrite = true;
185     if ( outputFile.exists() ) {
186         mayWrite = KMessageBox::questionYesNo(this, i18n("The output file <br/>%1<br/> already exists, "
187                                                          "do you want to overwrite it?",
188                                                           outputFile.fileName())) == KMessageBox::Yes;
189     }
190     if ( mayWrite ) {
191         auto url = QUrl::fromLocalFile(outputFile.fileName());
192         Q_ASSERT(url.isLocalFile());
193         auto basePath = url.url(QUrl::RemoveFilename | QUrl::PreferLocalFile);
194 
195         // should have been done previously
196         Q_ASSERT(QDir(basePath).exists());
197         if ( ! QDir(basePath).exists() ) {
198             QDir(basePath).mkpath(basePath);
199         }
200         outputFile.open(QIODevice::WriteOnly);
201         QString header = "\"\"\"" + i18n("This file contains auto-generated documentation extracted\n"
202                                          "from python run-time information. It is analyzed by KDevelop\n"
203                                          "to offer features such as code-completion and syntax highlighting.\n"
204                                          "If you discover errors in KDevelop's support for this module,\n"
205                                          "you can edit this file to correct the errors, e.g. you can add\n"
206                                          "additional return statements to functions to control the return\n"
207                                          "type to be used for that function by the analyzer.\n"
208                                          "Make sure to keep a copy of your changes so you don't accidentally\n"
209                                          "overwrite them by re-generating the file.\n") + "\"\"\"\n\n";
210         outputFile.write(header.toUtf8() + resultField->toPlainText().toUtf8());
211         outputFile.close();
212         savedAs = outputFile.fileName();
213         close();
214     }
215 }
216 
processScriptOutput()217 void DocfileWizard::processScriptOutput()
218 {
219     statusField->insertPlainText(worker->readAllStandardError());
220     resultField->insertPlainText(worker->readAllStandardOutput());
221     QScrollBar* scrollbar = statusField->verticalScrollBar();
222     scrollbar->setValue(scrollbar->maximum());
223 }
224 
processFinished(int,QProcess::ExitStatus)225 void DocfileWizard::processFinished(int, QProcess::ExitStatus)
226 {
227     worker = nullptr;
228     runButton->setEnabled(true);
229     saveButton->setEnabled(true);
230 }
231 
updateOutputFilename(const QString & newModuleName)232 void DocfileWizard::updateOutputFilename(const QString& newModuleName)
233 {
234     QString newFileName = fileNameForModule(newModuleName);
235     if ( fileNameForModule(previousModuleName) == outputFilenameField->text() ) {
236         // the user didn't edit the field, or edited it to what it is anyways, so update the text
237         // otherwise, do nothing.
238         outputFilenameField->setText(newFileName);
239     }
240     previousModuleName = newModuleName;
241 }
242