Source code

Revision control

Copy as Markdown

// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant
#include "qqmldebugtranslationservice.h"
#include "proxytranslator.h"
#include "qqmlpreviewservice.h"
#include <QtCore/qtranslator.h>
#include <QtCore/qdebug.h>
#include <QtCore/qlibraryinfo.h>
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtCore/qtimer.h>
#include <QtCore/qhash.h>
#include <QtCore/qpointer.h>
#include <private/qqmldebugtranslationprotocol_p.h>
#include <private/qqmldebugconnector_p.h>
#include <private/qversionedpacket_p.h>
#include <private/qqmlbinding_p.h>
#include <private/qqmlbinding_p.h>
#include <private/qquickstategroup_p.h>
#include <private/qquickitem_p.h>
#include <private/qquicktext_p.h>
#include <private/qdebug_p.h>
#include <QtQuick/qquickitem.h>
#include <qquickview.h>
QT_BEGIN_NAMESPACE
using namespace QQmlDebugTranslation;
QDebug operator<<(QDebug debug, const TranslationBindingInformation &translationBindingInformation)
{
QQmlError error;
error.setUrl(translationBindingInformation.compilationUnit->url());
error.setLine(translationBindingInformation.line);
error.setColumn(translationBindingInformation.column);
error.setDescription(
QString(QLatin1String(
"QDebug translation binding"
)));
return debug << qPrintable(error.toString());
}
static QString translationIdString(const QQmlTranslation::QsTrData &data)
{
return QString::fromUtf8(data.text());
}
static QString translationIdString(const QQmlTranslation::QsTrIdData &data)
{
return QString::fromUtf8(data.id());
}
static QString translationIdString(const QQmlTranslation &translation)
{
return translation.visit(
[](auto &&arg) -> QString {
using T = std::decay_t<decltype(arg)>;
if constexpr (!std::is_same_v<T, std::nullptr_t>)
return translationIdString(arg);
else {
Q_ASSERT_X(false, "QQmlTranslation", "Uninitialized Translation");
return {};
}
});
}
class QQmlDebugTranslationServicePrivate : public QObject
{
Q_OBJECT
public:
QQmlDebugTranslationServicePrivate(QQmlDebugTranslationServiceImpl *parent)
: q(parent)
, proxyTranslator(new ProxyTranslator)
{
connect(&translatableTextOccurrenceTimer, &QTimer::timeout,
this, &QQmlDebugTranslationServicePrivate::sendTranslatableTextOccurrences);
}
void setState(const QString &stateName)
{
if (QQuickItem *rootItem = currentRootItem()) {
QQuickStateGroup *stateGroup = QQuickItemPrivate::get(rootItem)->_states();
if (stateGroup->findState(stateName)) {
connect(stateGroup, &QQuickStateGroup::stateChanged,
this, &QQmlDebugTranslationServicePrivate::sendStateChanged,
Qt::ConnectionType(Qt::QueuedConnection | Qt::UniqueConnection));
stateGroup->setState(stateName);
}
else
qWarning() << "Could not switch the state" << stateName << "at" << rootItem;
}
}
void sendStateChanged()
{
if (QQuickStateGroup *stateGroup = qobject_cast<QQuickStateGroup*>(sender()))
currentStateName = stateGroup->state();
QVersionedPacket<QQmlDebugConnector> packet;
packet << Reply::StateChanged << currentStateName;
emit q->messageToClient(q->name(), packet.data());
}
void sendStateList()
{
QVersionedPacket<QQmlDebugConnector> packet;
packet << Reply::StateList;
QList<QmlState> qmlStates;
if (QQuickItem *rootItem = currentRootItem()) {
QQuickStateGroup *stateGroup = QQuickItemPrivate::get(rootItem)->_states();
QList<QQuickState *> states = stateGroup->states();
for (QQuickState *state : states) {
QmlState qmlState;
qmlState.name = state->name();
qmlStates.append(qmlState);
}
}
packet << qmlStates;
emit q->messageToClient(q->name(), packet.data());
}
void setWatchTextElides(bool s)
{
// TODO: for disabling we need to keep track which one were enabled
if (s == false)
qWarning() << "disable WatchTextElides is not implemented";
watchTextElides = s;
for (auto &&information : std::as_const(objectTranslationBindingMultiMap)) {
QObject *scopeObject = information.scopeObject;
int elideIndex = scopeObject->metaObject()->indexOfProperty("elide");
if (elideIndex >= 0) {
auto elideProperty = scopeObject->metaObject()->property(elideIndex);
elideProperty.write(scopeObject, Qt::ElideRight);
}
}
}
QString getStyleNameForFont(const QFont& font)
{
if (font.styleName() != "")
return font.styleName();
QString styleName;
if (font.bold())
styleName.append("Bold ");
if (font.italic())
styleName.append("Italic " );
if (font.strikeOut())
styleName.append("StrikeThrough ");
if (font.underline())
styleName.append("Underline ");
return styleName.trimmed();
}
void sendTranslatableTextOccurrences()
{
QVersionedPacket<QQmlDebugConnector> packet;
packet << Reply::TranslatableTextOccurrences;
QList<QmlElement> qmlElements;
for (auto &&information : std::as_const(objectTranslationBindingMultiMap)) {
QObject *scopeObject = information.scopeObject;
auto compilationUnit = information.compilationUnit;
auto metaObject = scopeObject->metaObject();
int textIndex = metaObject->indexOfProperty(information.propertyName.toLatin1());
if (textIndex >= 0) {
QmlElement qmlElement;
qmlElement.codeMarker = codeMarker(information);
auto textProperty = scopeObject->metaObject()->property(textIndex);
qmlElement.propertyName = textProperty.name();
qmlElement.translationId = translationIdString(information.translation);
qmlElement.translatedText = textProperty.read(scopeObject).toString();
qmlElement.elementId = qmlContext(scopeObject)->nameForObject(scopeObject);
QFont font = scopeObject->property("font").value<QFont>();
qmlElement.fontFamily = font.family();
qmlElement.fontPointSize = font.pointSize();
qmlElement.fontPixelSize = font.pixelSize();
qmlElement.fontStyleName = getStyleNameForFont(font);
qmlElement.horizontalAlignment =
scopeObject->property("horizontalAlignment").toInt();
qmlElement.verticalAlignment = scopeObject->property("verticalAlignment").toInt();
QQmlType qmlType = QQmlMetaType::qmlType(metaObject);
qmlElement.elementType = qmlType.qmlTypeName() + "/" + qmlType.typeName();
qmlElement.stateName = currentStateName;
qmlElements.append(qmlElement);
} else {
QString warningMessage = "(QQmlDebugTranslationService can not resolve %1 - %2:"\
" this should never happen)";
const QString id = qmlContext(scopeObject)->nameForObject(scopeObject);
qWarning().noquote() << warningMessage.arg(id, information.propertyName);
}
}
std::sort(qmlElements.begin(), qmlElements.end(), [](const auto &l1, const auto &l2){
return l1.codeMarker < l2.codeMarker;
});
packet << qmlElements;
emit q->messageToClient(q->name(), packet.data());
}
void sendLanguageChanged()
{
QVersionedPacket<QQmlDebugConnector> packet;
packet << Reply::LanguageChanged;
emit q->messageToClient(q->name(), packet.data());
}
void sendTranslationIssues()
{
QVersionedPacket<QQmlDebugConnector> packet;
packet << Reply::TranslationIssues;
QList<TranslationIssue> issues;
for (auto &&information : std::as_const(objectTranslationBindingMultiMap)) {
if (!proxyTranslator->hasTranslation(information)) {
TranslationIssue issue;
issue.type = TranslationIssue::Type::Missing;
issue.codeMarker = codeMarker(information);
issue.language = proxyTranslator->currentUILanguages();
issues.append(issue);
}
QObject *scopeObject = information.scopeObject;
QQuickText *quickText = static_cast<QQuickText*>(scopeObject);
if (quickText) {
if (quickText->truncated()) {
TranslationIssue issue;
issue.type = TranslationIssue::Type::Elided;
issue.codeMarker = codeMarker(information);
issue.language = proxyTranslator->currentUILanguages();
issues.append(issue);
}
}
}
std::sort(issues.begin(), issues.end(), [](const auto &l1, const auto &l2){
return l1.codeMarker < l2.codeMarker;
});
packet << issues;
emit q->messageToClient(q->name(), packet.data());
}
QQmlDebugTranslationServiceImpl *q;
bool watchTextElides = false;
QMultiMap<QObject*, TranslationBindingInformation> objectTranslationBindingMultiMap;
QHash<QObject*, QList<QMetaObject::Connection>> elideConnections;
ProxyTranslator *proxyTranslator;
bool enableWatchTranslations = false;
QTimer translatableTextOccurrenceTimer;
QList<QPointer<QQuickItem>> translatableTextOccurrences;
QQuickItem *currentRootItem()
{
if (QQmlPreviewServiceImpl *service = QQmlDebugConnector::service<QQmlPreviewServiceImpl>())
return service->currentRootItem();
if (currentQuickView)
return currentQuickView->rootObject();
return nullptr;
}
QQuickView* currentQuickView = nullptr;
private:
CodeMarker codeMarker(const TranslationBindingInformation &information)
{
CodeMarker c;
c.url = information.compilationUnit->url();
c.line = information.line;
c.column = information.column;
return c;
}
QString currentStateName;
};
QQmlDebugTranslationServiceImpl::QQmlDebugTranslationServiceImpl(QObject *parent)
: QQmlDebugTranslationService(1, parent)
{
d = new QQmlDebugTranslationServicePrivate(this);
connect(this, &QQmlDebugTranslationServiceImpl::watchTextElides,
d, &QQmlDebugTranslationServicePrivate::setWatchTextElides,
Qt::QueuedConnection);
connect(this, &QQmlDebugTranslationServiceImpl::language,
d->proxyTranslator, &ProxyTranslator::setLanguage,
Qt::QueuedConnection);
connect(this, &QQmlDebugTranslationServiceImpl::state,
d, &QQmlDebugTranslationServicePrivate::setState,
Qt::QueuedConnection);
connect(this, &QQmlDebugTranslationServiceImpl::stateList,
d, &QQmlDebugTranslationServicePrivate::sendStateList,
Qt::QueuedConnection);
connect(d->proxyTranslator, &ProxyTranslator::languageChanged,
d, &QQmlDebugTranslationServicePrivate::sendLanguageChanged,
Qt::QueuedConnection);
connect(this, &QQmlDebugTranslationServiceImpl::translationIssues,
d, &QQmlDebugTranslationServicePrivate::sendTranslationIssues,
Qt::QueuedConnection);
connect(this, &QQmlDebugTranslationServiceImpl::sendTranslatableTextOccurrences,
d, &QQmlDebugTranslationServicePrivate::sendTranslatableTextOccurrences,
Qt::QueuedConnection);
}
QQmlDebugTranslationServiceImpl::~QQmlDebugTranslationServiceImpl()
{
delete d->proxyTranslator;
d->proxyTranslator = {};
delete d;
}
void QQmlDebugTranslationServiceImpl::messageReceived(const QByteArray &message)
{
QVersionedPacket<QQmlDebugConnector> packet(message);
QQmlDebugTranslation::Request command;
packet >> command;
switch (command) {
case QQmlDebugTranslation::Request::ChangeLanguage: {
QUrl context;
QString locale;
packet >> context >> locale;
emit language(context, QLocale(locale));
break;
}
case QQmlDebugTranslation::Request::ChangeState: {
QString stateName;
packet >> stateName;
emit state(stateName);
break;
}
case QQmlDebugTranslation::Request::StateList: {
emit stateList();
break;
}
case QQmlDebugTranslation::Request::TranslationIssues: {
emit translationIssues();
break;
}
case QQmlDebugTranslation::Request::TranslatableTextOccurrences: {
emit sendTranslatableTextOccurrences();
break;
}
case QQmlDebugTranslation::Request::WatchTextElides: {
emit watchTextElides(true);
break;
}
case QQmlDebugTranslation::Request::DisableWatchTextElides: {
emit watchTextElides(false);
break;
}
default: {
qWarning() << "DebugTranslationService: received unknown command: " << static_cast<int>(command);
break;
}
} // switch (command)
}
void QQmlDebugTranslationServiceImpl::engineAboutToBeAdded(QJSEngine *engine)
{
if (QQmlEngine *qmlEngine = qobject_cast<QQmlEngine *>(engine))
d->proxyTranslator->addEngine(qmlEngine);
if (engine->parent())
d->currentQuickView = qobject_cast<QQuickView*>(engine->parent());
emit attachedToEngine(engine);
}
void QQmlDebugTranslationServiceImpl::engineAboutToBeRemoved(QJSEngine *engine)
{
if (QQmlEngine *qmlEngine = qobject_cast<QQmlEngine *>(engine))
d->proxyTranslator->removeEngine(qmlEngine);
emit detachedFromEngine(engine);
}
void QQmlDebugTranslationServiceImpl::foundTranslationBinding(const TranslationBindingInformation &translationBindingInformation)
{
QObject *scopeObject = translationBindingInformation.scopeObject;
connect(scopeObject, &QObject::destroyed, this, [this, scopeObject] () {
this->d->objectTranslationBindingMultiMap.remove(scopeObject);
});
d->objectTranslationBindingMultiMap.insert(scopeObject, translationBindingInformation);
}
QT_END_NAMESPACE
#include "moc_qqmldebugtranslationservice.cpp"
#include <qqmldebugtranslationservice.moc>