1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "pythonwriteimports.h"
5
6#include <customwidgetsinfo.h>
7#include <option.h>
8#include <uic.h>
9#include <driver.h>
10
11#include <ui4.h>
12
13#include <QtCore/qdir.h>
14#include <QtCore/qfileinfo.h>
15#include <QtCore/qtextstream.h>
16
17#include <algorithm>
18
19QT_BEGIN_NAMESPACE
20
21using namespace Qt::StringLiterals;
22
23// Generate imports for Python. Note some things differ from C++:
24// - qItemView->header()->setFoo() does not require QHeaderView to be imported
25// - qLabel->setFrameShape(QFrame::Box) however requires QFrame to be imported
26// (see acceptProperty())
27
28namespace Python {
29
30// Classes required for properties
31static WriteImports::ClassesPerModule defaultClasses()
32{
33 return {
34 {QStringLiteral("QtCore"),
35 {QStringLiteral("QCoreApplication"), QStringLiteral("QDate"),
36 QStringLiteral("QDateTime"), QStringLiteral("QLocale"),
37 QStringLiteral("QMetaObject"), QStringLiteral("QObject"),
38 QStringLiteral("QPoint"), QStringLiteral("QRect"),
39 QStringLiteral("QSize"), QStringLiteral("QTime"),
40 QStringLiteral("QUrl"), QStringLiteral("Qt")},
41 },
42 {QStringLiteral("QtGui"),
43 {QStringLiteral("QBrush"), QStringLiteral("QColor"),
44 QStringLiteral("QConicalGradient"), QStringLiteral("QCursor"),
45 QStringLiteral("QGradient"), QStringLiteral("QFont"),
46 QStringLiteral("QFontDatabase"), QStringLiteral("QIcon"),
47 QStringLiteral("QImage"), QStringLiteral("QKeySequence"),
48 QStringLiteral("QLinearGradient"), QStringLiteral("QPalette"),
49 QStringLiteral("QPainter"), QStringLiteral("QPixmap"),
50 QStringLiteral("QTransform"), QStringLiteral("QRadialGradient")}
51 },
52 // Add QWidget for QWidget.setTabOrder()
53 {QStringLiteral("QtWidgets"),
54 {QStringLiteral("QSizePolicy"), QStringLiteral("QWidget")}
55 }
56 };
57}
58
59// Helpers for WriteImports::ClassesPerModule maps
60static void insertClass(const QString &module, const QString &className,
61 WriteImports::ClassesPerModule *c)
62{
63 auto usedIt = c->find(key: module);
64 if (usedIt == c->end())
65 c->insert(key: module, value: {className});
66 else if (!usedIt.value().contains(str: className))
67 usedIt.value().append(t: className);
68}
69
70// Format a class list: "from A import (B, C)"
71static void formatImportClasses(QTextStream &str, QStringList classList)
72{
73 std::sort(first: classList.begin(), last: classList.end());
74
75 const qsizetype size = classList.size();
76 if (size > 1)
77 str << '(';
78 for (qsizetype i = 0; i < size; ++i) {
79 if (i > 0)
80 str << (i % 4 == 0 ? ",\n " : ", ");
81 str << classList.at(i);
82 }
83 if (size > 1)
84 str << ')';
85}
86
87static void formatClasses(QTextStream &str, const WriteImports::ClassesPerModule &c,
88 bool useStarImports = false,
89 const QByteArray &modulePrefix = {})
90{
91 for (auto it = c.cbegin(), end = c.cend(); it != end; ++it) {
92 str << "from " << modulePrefix << it.key() << " import ";
93 if (useStarImports)
94 str << "* # type: ignore";
95 else
96 formatImportClasses(str, classList: it.value());
97 str << '\n';
98 }
99}
100
101WriteImports::WriteImports(Uic *uic) : WriteIncludesBase(uic),
102 m_qtClasses(defaultClasses())
103{
104 for (const auto &e : classInfoEntries())
105 m_classToModule.insert(key: QLatin1StringView(e.klass), value: QLatin1StringView(e.module));
106}
107
108void WriteImports::acceptUI(DomUI *node)
109{
110 WriteIncludesBase::acceptUI(node);
111
112 auto &output = uic()->output();
113 const bool useStarImports = uic()->driver()->option().useStarImports;
114
115 const QByteArray qtPrefix = QByteArrayLiteral("PySide")
116 + QByteArray::number(QT_VERSION_MAJOR) + '.';
117
118 formatClasses(str&: output, c: m_qtClasses, useStarImports, modulePrefix: qtPrefix);
119
120 if (!m_customWidgets.isEmpty() || !m_plainCustomWidgets.isEmpty()) {
121 output << '\n';
122 formatClasses(str&: output, c: m_customWidgets, useStarImports);
123 for (const auto &w : m_plainCustomWidgets)
124 output << "import " << w << '\n';
125 }
126
127 if (auto resources = node->elementResources()) {
128 const auto includes = resources->elementInclude();
129 for (auto include : includes) {
130 if (include->hasAttributeLocation())
131 writeResourceImport(module: include->attributeLocation());
132 }
133 output << '\n';
134 }
135}
136
137QString WriteImports::resourceAbsolutePath(QString resource) const
138{
139 // If we know the project root, generate an absolute Python import
140 // to the resource. options. pythonRoot is the Python path component
141 // under which the UI file is.
142 const auto &options = uic()->option();
143 if (!options.inputFile.isEmpty() && !options.pythonRoot.isEmpty()) {
144 resource = QDir::cleanPath(path: QFileInfo(options.inputFile).canonicalPath() + u'/' + resource);
145 if (resource.size() > options.pythonRoot.size())
146 resource.remove(i: 0, len: options.pythonRoot.size() + 1);
147 }
148 // If nothing is known, we assume the directory pointed by "../" is the root
149 while (resource.startsWith(s: u"../"))
150 resource.remove(i: 0, len: 3);
151 resource.replace(before: u'/', after: u'.');
152 return resource;
153}
154
155void WriteImports::writeResourceImport(const QString &module)
156{
157 const auto &options = uic()->option();
158 auto &str = uic()->output();
159
160 QString resource = QDir::cleanPath(path: module);
161 if (resource.endsWith(s: u".qrc"))
162 resource.chop(n: 4);
163 const qsizetype basePos = resource.lastIndexOf(c: u'/') + 1;
164 // Change the name of a qrc file "dir/foo.qrc" file to the Python
165 // module name "foo_rc" according to project conventions.
166 if (options.rcPrefix)
167 resource.insert(i: basePos, v: u"rc_");
168 else
169 resource.append(v: u"_rc");
170
171 switch (options.pythonResourceImport) {
172 case Option::PythonResourceImport::Default:
173 str << "import " << QStringView{resource}.sliced(pos: basePos) << '\n';
174 break;
175 case Option::PythonResourceImport::FromDot:
176 str << "from . import " << QStringView{resource}.sliced(pos: basePos) << '\n';
177 break;
178 case Option::PythonResourceImport::Absolute:
179 str << "import " << resourceAbsolutePath(resource) << '\n';
180 break;
181 }
182}
183
184void WriteImports::doAdd(const QString &className, const DomCustomWidget *dcw)
185{
186 const CustomWidgetsInfo *cwi = uic()->customWidgetsInfo();
187 if (cwi->extends(className, baseClassName: "QListWidget"))
188 add(QStringLiteral("QListWidgetItem"));
189 else if (cwi->extends(className, baseClassName: "QTreeWidget"))
190 add(QStringLiteral("QTreeWidgetItem"));
191 else if (cwi->extends(className, baseClassName: "QTableWidget"))
192 add(QStringLiteral("QTableWidgetItem"));
193
194 if (dcw != nullptr) {
195 addPythonCustomWidget(className, dcw);
196 return;
197 }
198
199 if (!addQtClass(className))
200 qWarning(msg: "WriteImports::add(): Unknown Qt class %s", qPrintable(className));
201}
202
203bool WriteImports::addQtClass(const QString &className)
204{
205 // QVariant is not exposed in PySide
206 if (className == u"QVariant" || className == u"Qt")
207 return true;
208
209 const auto moduleIt = m_classToModule.constFind(key: className);
210 const bool result = moduleIt != m_classToModule.cend();
211 if (result)
212 insertClass(module: moduleIt.value(), className, c: &m_qtClasses);
213 return result;
214}
215
216void WriteImports::addPythonCustomWidget(const QString &className, const DomCustomWidget *node)
217{
218 if (className.contains(s: "::"_L1))
219 return; // Exclude namespaced names (just to make tests pass).
220
221 if (addQtClass(className)) // Qt custom widgets like QQuickWidget, QAxWidget, etc
222 return;
223
224 // When the elementHeader is not set, we know it's the continuation
225 // of a Qt for Python import or a normal import of another module.
226 if (!node->elementHeader() || node->elementHeader()->text().isEmpty()) {
227 m_plainCustomWidgets.append(t: className);
228 } else { // When we do have elementHeader, we know it's a relative import.
229 QString modulePath = node->elementHeader()->text();
230 // Replace the '/' by '.'
231 modulePath.replace(before: u'/', after: u'.');
232 // '.h' is added by default on headers for <customwidget>
233 if (modulePath.endsWith(s: ".h"_L1))
234 modulePath.chop(n: 2);
235 insertClass(module: modulePath, className, c: &m_customWidgets);
236 }
237}
238
239void WriteImports::acceptProperty(DomProperty *node)
240{
241 switch (node->kind()) {
242 case DomProperty::Enum:
243 addEnumBaseClass(v: node->elementEnum());
244 break;
245 case DomProperty::Set:
246 addEnumBaseClass(v: node->elementSet());
247 break;
248 default:
249 break;
250 }
251
252 WriteIncludesBase::acceptProperty(node);
253}
254
255void WriteImports::addEnumBaseClass(const QString &v)
256{
257 // Add base classes like QFrame for QLabel::frameShape()
258 const auto colonPos = v.indexOf(s: u"::");
259 if (colonPos > 0) {
260 const QString base = v.left(n: colonPos);
261 if (base.startsWith(c: u'Q') && base != u"Qt")
262 addQtClass(className: base);
263 }
264}
265
266} // namespace Python
267
268QT_END_NAMESPACE
269

source code of qtbase/src/tools/uic/python/pythonwriteimports.cpp