| 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 , cs: Qt::CaseInsensitive)) |
| 234 | modulePath.chop(n: 2); |
| 235 | else if (modulePath.endsWith(s: ".hh"_L1 )) |
| 236 | modulePath.chop(n: 3); |
| 237 | else if (modulePath.endsWith(s: ".hpp"_L1 )) |
| 238 | modulePath.chop(n: 4); |
| 239 | insertClass(module: modulePath, className, c: &m_customWidgets); |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | void WriteImports::acceptProperty(DomProperty *node) |
| 244 | { |
| 245 | switch (node->kind()) { |
| 246 | case DomProperty::Enum: |
| 247 | addEnumBaseClass(v: node->elementEnum()); |
| 248 | break; |
| 249 | case DomProperty::Set: |
| 250 | addEnumBaseClass(v: node->elementSet()); |
| 251 | break; |
| 252 | default: |
| 253 | break; |
| 254 | } |
| 255 | |
| 256 | WriteIncludesBase::acceptProperty(node); |
| 257 | } |
| 258 | |
| 259 | void WriteImports::addEnumBaseClass(const QString &v) |
| 260 | { |
| 261 | // Add base classes like QFrame for QLabel::frameShape() |
| 262 | const auto colonPos = v.indexOf(s: u"::" ); |
| 263 | if (colonPos > 0) { |
| 264 | const QString base = v.left(n: colonPos); |
| 265 | if (base.startsWith(c: u'Q') && base != u"Qt" ) |
| 266 | addQtClass(className: base); |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | } // namespace Python |
| 271 | |
| 272 | QT_END_NAMESPACE |
| 273 | |