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
49class QQmlDebugTranslationServicePrivate : public QObject
50{
51 Q_OBJECT
52public:
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
257private:
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
269QQmlDebugTranslationServiceImpl::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
303QQmlDebugTranslationServiceImpl::~QQmlDebugTranslationServiceImpl()
304{
305 delete d->proxyTranslator;
306 d->proxyTranslator = {};
307}
308
309void 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
356void 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
367void 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
374void 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
384QT_END_NAMESPACE
385
386#include "moc_qqmldebugtranslationservice.cpp"
387
388#include <qqmldebugtranslationservice.moc>
389

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