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