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

source code of qtdeclarative/src/plugins/qmltooling/qmldbg_preview/qqmldebugtranslationservice.cpp