1 | // Copyright (C) 2020 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qqmldebugtranslationservice.h" |
5 | #include "proxytranslator.h" |
6 | #include "qqmlpreviewservice.h" |
7 | |
8 | #include <QtCore/qtranslator.h> |
9 | #include <QtCore/qdebug.h> |
10 | #include <QtCore/qlibraryinfo.h> |
11 | #include <QtCore/qdir.h> |
12 | #include <QtCore/qfile.h> |
13 | #include <QtCore/qtimer.h> |
14 | #include <QtCore/qhash.h> |
15 | #include <QtCore/qpointer.h> |
16 | |
17 | #include <private/qqmldebugtranslationprotocol_p.h> |
18 | #include <private/qqmldebugconnector_p.h> |
19 | #include <private/qversionedpacket_p.h> |
20 | |
21 | #include <private/qqmlbinding_p.h> |
22 | #include <private/qqmlbinding_p.h> |
23 | #include <private/qquickstategroup_p.h> |
24 | #include <private/qquickitem_p.h> |
25 | #include <private/qquicktext_p.h> |
26 | #include <private/qdebug_p.h> |
27 | |
28 | #include <QtQuick/qquickitem.h> |
29 | |
30 | #include <qquickview.h> |
31 | |
32 | QT_BEGIN_NAMESPACE |
33 | |
34 | using namespace QQmlDebugTranslation; |
35 | |
36 | QDebug operator<<(QDebug debug, const TranslationBindingInformation &translationBindingInformation) |
37 | { |
38 | QQmlError error; |
39 | error.setUrl(translationBindingInformation.compilationUnit->url()); |
40 | error.setLine(translationBindingInformation.line); |
41 | error.setColumn(translationBindingInformation.column); |
42 | error.setDescription( |
43 | QString(QLatin1String( |
44 | "QDebug translation binding" |
45 | ))); |
46 | return debug << qPrintable(error.toString()); |
47 | } |
48 | |
49 | class QQmlDebugTranslationServicePrivate : public QObject |
50 | { |
51 | Q_OBJECT |
52 | public: |
53 | QQmlDebugTranslationServicePrivate(QQmlDebugTranslationServiceImpl *parent) |
54 | : q(parent) |
55 | , proxyTranslator(new ProxyTranslator) |
56 | { |
57 | connect(sender: &translatableTextOccurrenceTimer, signal: &QTimer::timeout, |
58 | context: this, slot: &QQmlDebugTranslationServicePrivate::sendTranslatableTextOccurrences); |
59 | } |
60 | |
61 | void setState(const QString &stateName) |
62 | { |
63 | if (QQuickItem *rootItem = currentRootItem()) { |
64 | QQuickStateGroup *stateGroup = QQuickItemPrivate::get(item: rootItem)->_states(); |
65 | if (stateGroup->findState(name: stateName)) { |
66 | connect(sender: stateGroup, signal: &QQuickStateGroup::stateChanged, |
67 | context: this, slot: &QQmlDebugTranslationServicePrivate::sendStateChanged, |
68 | type: Qt::ConnectionType(Qt::QueuedConnection | Qt::UniqueConnection)); |
69 | stateGroup->setState(stateName); |
70 | } |
71 | else |
72 | qWarning() << "Could not switch the state" << stateName << "at" << rootItem; |
73 | } |
74 | } |
75 | |
76 | void sendStateChanged() |
77 | { |
78 | if (QQuickStateGroup *stateGroup = qobject_cast<QQuickStateGroup*>(object: sender())) |
79 | currentStateName = stateGroup->state(); |
80 | QVersionedPacket<QQmlDebugConnector> packet; |
81 | packet << Reply::StateChanged << currentStateName; |
82 | emit q->messageToClient(name: q->name(), message: packet.data()); |
83 | } |
84 | |
85 | void sendStateList() |
86 | { |
87 | QVersionedPacket<QQmlDebugConnector> packet; |
88 | packet << Reply::StateList; |
89 | QVector<QmlState> qmlStates; |
90 | |
91 | if (QQuickItem *rootItem = currentRootItem()) { |
92 | QQuickStateGroup *stateGroup = QQuickItemPrivate::get(item: rootItem)->_states(); |
93 | |
94 | QList<QQuickState *> states = stateGroup->states(); |
95 | |
96 | for (QQuickState *state : states) { |
97 | QmlState qmlState; |
98 | qmlState.name = state->name(); |
99 | qmlStates.append(t: qmlState); |
100 | } |
101 | } |
102 | |
103 | packet << qmlStates; |
104 | emit q->messageToClient(name: q->name(), message: packet.data()); |
105 | } |
106 | |
107 | void setWatchTextElides(bool s) |
108 | { |
109 | // TODO: for disabling we need to keep track which one were enabled |
110 | if (s == false) |
111 | qWarning() << "disable WatchTextElides is not implemented" ; |
112 | watchTextElides = s; |
113 | for (auto &&information : std::as_const(t&: objectTranslationBindingMultiMap)) { |
114 | QObject *scopeObject = information.scopeObject; |
115 | int elideIndex = scopeObject->metaObject()->indexOfProperty(name: "elide" ); |
116 | if (elideIndex >= 0) { |
117 | auto elideProperty = scopeObject->metaObject()->property(index: elideIndex); |
118 | elideProperty.write(obj: scopeObject, value: Qt::ElideRight); |
119 | } |
120 | } |
121 | } |
122 | |
123 | QString getStyleNameForFont(const QFont& font) |
124 | { |
125 | if (font.styleName() != "" ) |
126 | return font.styleName(); |
127 | QString styleName; |
128 | if (font.bold()) |
129 | styleName.append(s: "Bold " ); |
130 | if (font.italic()) |
131 | styleName.append(s: "Italic " ); |
132 | if (font.strikeOut()) |
133 | styleName.append(s: "StrikeThrough " ); |
134 | if (font.underline()) |
135 | styleName.append(s: "Underline " ); |
136 | return styleName.trimmed(); |
137 | } |
138 | |
139 | void sendTranslatableTextOccurrences() |
140 | { |
141 | |
142 | QVersionedPacket<QQmlDebugConnector> packet; |
143 | packet << Reply::TranslatableTextOccurrences; |
144 | |
145 | QVector<QmlElement> qmlElements; |
146 | |
147 | for (auto &&information : std::as_const(t&: objectTranslationBindingMultiMap)) { |
148 | |
149 | QObject *scopeObject = information.scopeObject; |
150 | auto compilationUnit = information.compilationUnit; |
151 | auto metaObject = scopeObject->metaObject(); |
152 | |
153 | int textIndex = metaObject->indexOfProperty(name: information.propertyName.toLatin1()); |
154 | if (textIndex >= 0) { |
155 | |
156 | QmlElement qmlElement; |
157 | |
158 | qmlElement.codeMarker = codeMarker(information); |
159 | |
160 | auto textProperty = scopeObject->metaObject()->property(index: textIndex); |
161 | qmlElement.propertyName = textProperty.name(); |
162 | qmlElement.translationId = information.translation.idForQmlDebug(); |
163 | qmlElement.translatedText = textProperty.read(obj: scopeObject).toString(); |
164 | qmlElement.elementId = qmlContext(scopeObject)->nameForObject(scopeObject); |
165 | |
166 | QFont font = scopeObject->property(name: "font" ).value<QFont>(); |
167 | qmlElement.fontFamily = font.family(); |
168 | qmlElement.fontPointSize = font.pointSize(); |
169 | qmlElement.fontPixelSize = font.pixelSize(); |
170 | qmlElement.fontStyleName = getStyleNameForFont(font); |
171 | qmlElement.horizontalAlignment = |
172 | scopeObject->property(name: "horizontalAlignment" ).toInt(); |
173 | qmlElement.verticalAlignment = scopeObject->property(name: "verticalAlignment" ).toInt(); |
174 | |
175 | QQmlType qmlType = QQmlMetaType::qmlType(metaObject); |
176 | qmlElement.elementType = qmlType.qmlTypeName() + "/" + qmlType.typeName(); |
177 | qmlElement.stateName = currentStateName; |
178 | qmlElements.append(t: qmlElement); |
179 | |
180 | } else { |
181 | QString warningMessage = "(QQmlDebugTranslationService can not resolve %1 - %2:" \ |
182 | " this should never happen)" ; |
183 | const QString id = qmlContext(scopeObject)->nameForObject(scopeObject); |
184 | qWarning().noquote() << warningMessage.arg(args: id, args: information.propertyName); |
185 | } |
186 | } |
187 | std::sort(first: qmlElements.begin(), last: qmlElements.end(), comp: [](const auto &l1, const auto &l2){ |
188 | return l1.codeMarker < l2.codeMarker; |
189 | }); |
190 | |
191 | packet << qmlElements; |
192 | emit q->messageToClient(name: q->name(), message: packet.data()); |
193 | } |
194 | |
195 | void sendLanguageChanged() |
196 | { |
197 | QVersionedPacket<QQmlDebugConnector> packet; |
198 | packet << Reply::LanguageChanged; |
199 | emit q->messageToClient(name: q->name(), message: packet.data()); |
200 | } |
201 | |
202 | void sendTranslationIssues() |
203 | { |
204 | QVersionedPacket<QQmlDebugConnector> packet; |
205 | packet << Reply::TranslationIssues; |
206 | |
207 | QVector<TranslationIssue> issues; |
208 | for (auto &&information : std::as_const(t&: objectTranslationBindingMultiMap)) { |
209 | if (!proxyTranslator->hasTranslation(translationBindingInformation: information)) { |
210 | TranslationIssue issue; |
211 | issue.type = TranslationIssue::Type::Missing; |
212 | issue.codeMarker = codeMarker(information); |
213 | issue.language = proxyTranslator->currentUILanguages(); |
214 | issues.append(t: issue); |
215 | } |
216 | |
217 | QObject *scopeObject = information.scopeObject; |
218 | QQuickText *quickText = static_cast<QQuickText*>(scopeObject); |
219 | if (quickText) { |
220 | if (quickText->truncated()) { |
221 | TranslationIssue issue; |
222 | issue.type = TranslationIssue::Type::Elided; |
223 | issue.codeMarker = codeMarker(information); |
224 | issue.language = proxyTranslator->currentUILanguages(); |
225 | issues.append(t: issue); |
226 | } |
227 | } |
228 | } |
229 | std::sort(first: issues.begin(), last: issues.end(), comp: [](const auto &l1, const auto &l2){ |
230 | return l1.codeMarker < l2.codeMarker; |
231 | }); |
232 | packet << issues; |
233 | emit q->messageToClient(name: q->name(), message: packet.data()); |
234 | } |
235 | |
236 | QQmlDebugTranslationServiceImpl *q; |
237 | |
238 | bool watchTextElides = false; |
239 | QMultiMap<QObject*, TranslationBindingInformation> objectTranslationBindingMultiMap; |
240 | QHash<QObject*, QVector<QMetaObject::Connection>> elideConnections; |
241 | ProxyTranslator *proxyTranslator; |
242 | |
243 | bool enableWatchTranslations = false; |
244 | QTimer translatableTextOccurrenceTimer; |
245 | QList<QPointer<QQuickItem>> translatableTextOccurrences; |
246 | |
247 | QQuickItem *currentRootItem() |
248 | { |
249 | if (QQmlPreviewServiceImpl *service = QQmlDebugConnector::service<QQmlPreviewServiceImpl>()) |
250 | return service->currentRootItem(); |
251 | if (currentQuickView) |
252 | return currentQuickView->rootObject(); |
253 | return nullptr; |
254 | } |
255 | QQuickView* currentQuickView = nullptr; |
256 | |
257 | private: |
258 | CodeMarker codeMarker(const TranslationBindingInformation &information) |
259 | { |
260 | CodeMarker c; |
261 | c.url = information.compilationUnit->url(); |
262 | c.line = information.line; |
263 | c.column = information.column; |
264 | return c; |
265 | } |
266 | QString currentStateName; |
267 | }; |
268 | |
269 | QQmlDebugTranslationServiceImpl::QQmlDebugTranslationServiceImpl(QObject *parent) |
270 | : QQmlDebugTranslationService(1, parent) |
271 | { |
272 | d = new QQmlDebugTranslationServicePrivate(this); |
273 | |
274 | connect(sender: this, signal: &QQmlDebugTranslationServiceImpl::watchTextElides, |
275 | context: d, slot: &QQmlDebugTranslationServicePrivate::setWatchTextElides, |
276 | type: Qt::QueuedConnection); |
277 | |
278 | connect(sender: this, signal: &QQmlDebugTranslationServiceImpl::language, |
279 | context: d->proxyTranslator, slot: &ProxyTranslator::setLanguage, |
280 | type: Qt::QueuedConnection); |
281 | |
282 | connect(sender: this, signal: &QQmlDebugTranslationServiceImpl::state, |
283 | context: d, slot: &QQmlDebugTranslationServicePrivate::setState, |
284 | type: Qt::QueuedConnection); |
285 | |
286 | connect(sender: this, signal: &QQmlDebugTranslationServiceImpl::stateList, |
287 | context: d, slot: &QQmlDebugTranslationServicePrivate::sendStateList, |
288 | type: Qt::QueuedConnection); |
289 | |
290 | connect(sender: d->proxyTranslator, signal: &ProxyTranslator::languageChanged, |
291 | context: d, slot: &QQmlDebugTranslationServicePrivate::sendLanguageChanged, |
292 | type: Qt::QueuedConnection); |
293 | |
294 | connect(sender: this, signal: &QQmlDebugTranslationServiceImpl::translationIssues, |
295 | context: d, slot: &QQmlDebugTranslationServicePrivate::sendTranslationIssues, |
296 | type: Qt::QueuedConnection); |
297 | |
298 | connect(sender: this, signal: &QQmlDebugTranslationServiceImpl::sendTranslatableTextOccurrences, |
299 | context: d, slot: &QQmlDebugTranslationServicePrivate::sendTranslatableTextOccurrences, |
300 | type: Qt::QueuedConnection); |
301 | } |
302 | |
303 | QQmlDebugTranslationServiceImpl::~QQmlDebugTranslationServiceImpl() |
304 | { |
305 | delete d->proxyTranslator; |
306 | d->proxyTranslator = {}; |
307 | } |
308 | |
309 | void QQmlDebugTranslationServiceImpl::messageReceived(const QByteArray &message) |
310 | { |
311 | QVersionedPacket<QQmlDebugConnector> packet(message); |
312 | QQmlDebugTranslation::Request command; |
313 | |
314 | packet >> command; |
315 | switch (command) { |
316 | case QQmlDebugTranslation::Request::ChangeLanguage: { |
317 | QUrl context; |
318 | QString locale; |
319 | packet >> context >> locale; |
320 | emit language(context, locale: QLocale(locale)); |
321 | break; |
322 | } |
323 | case QQmlDebugTranslation::Request::ChangeState: { |
324 | QString stateName; |
325 | packet >> stateName; |
326 | emit state(stateName); |
327 | break; |
328 | } |
329 | case QQmlDebugTranslation::Request::StateList: { |
330 | emit stateList(); |
331 | break; |
332 | } |
333 | case QQmlDebugTranslation::Request::TranslationIssues: { |
334 | emit translationIssues(); |
335 | break; |
336 | } |
337 | case QQmlDebugTranslation::Request::TranslatableTextOccurrences: { |
338 | emit sendTranslatableTextOccurrences(); |
339 | break; |
340 | } |
341 | case QQmlDebugTranslation::Request::WatchTextElides: { |
342 | emit watchTextElides(true); |
343 | break; |
344 | } |
345 | case QQmlDebugTranslation::Request::DisableWatchTextElides: { |
346 | emit watchTextElides(false); |
347 | break; |
348 | } |
349 | default: { |
350 | qWarning() << "DebugTranslationService: received unknown command: " << static_cast<int>(command); |
351 | break; |
352 | } |
353 | } // switch (command) |
354 | } |
355 | |
356 | void QQmlDebugTranslationServiceImpl::engineAboutToBeAdded(QJSEngine *engine) |
357 | { |
358 | if (QQmlEngine *qmlEngine = qobject_cast<QQmlEngine *>(object: engine)) |
359 | d->proxyTranslator->addEngine(engine: qmlEngine); |
360 | |
361 | if (engine->parent()) |
362 | d->currentQuickView = qobject_cast<QQuickView*>(object: engine->parent()); |
363 | |
364 | emit attachedToEngine(engine); |
365 | } |
366 | |
367 | void QQmlDebugTranslationServiceImpl::engineAboutToBeRemoved(QJSEngine *engine) |
368 | { |
369 | if (QQmlEngine *qmlEngine = qobject_cast<QQmlEngine *>(object: engine)) |
370 | d->proxyTranslator->removeEngine(engine: qmlEngine); |
371 | emit detachedFromEngine(engine); |
372 | } |
373 | |
374 | void QQmlDebugTranslationServiceImpl::foundTranslationBinding(const TranslationBindingInformation &translationBindingInformation) |
375 | { |
376 | QObject *scopeObject = translationBindingInformation.scopeObject; |
377 | connect(sender: scopeObject, signal: &QObject::destroyed, slot: [this, scopeObject] () { |
378 | this->d->objectTranslationBindingMultiMap.remove(key: scopeObject); |
379 | }); |
380 | |
381 | d->objectTranslationBindingMultiMap.insert(key: scopeObject, value: translationBindingInformation); |
382 | } |
383 | |
384 | QT_END_NAMESPACE |
385 | |
386 | #include "moc_qqmldebugtranslationservice.cpp" |
387 | |
388 | #include <qqmldebugtranslationservice.moc> |
389 | |