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 | |
19 | QT_BEGIN_NAMESPACE |
20 | |
21 | using 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 | |
28 | namespace Python { |
29 | |
30 | // Classes required for properties |
31 | static 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 |
60 | static 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)" |
71 | static 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 | |
87 | static 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 | |
101 | WriteImports::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 | |
108 | void 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 | |
137 | QString 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 | |
155 | void 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 | |
184 | void 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 | |
203 | bool 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 | |
216 | void 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 | |
239 | void 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 | |
255 | void 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 | |
268 | QT_END_NAMESPACE |
269 | |