Skip to content

Commit bd736f3

Browse files
committed
linguist: preview QML files with translation
If a translation file corresponds to a Qml document, show a preview of the qml document that contains the translations. Knwon limitations: - arguments in texts: "Hello %1" linguist can only see the runtime version of the string and fails to match it with an existing message in TS file. - items in comboboxes: This needs a bit of more implementation (possibly next iterations), since combobox stores items in the "model" property, rather than "text". Also if implemented, it will not be available to .qml files, but only provides support for .ui.qml files. Since normal Qml files are opened in a frozen state (no interactions with the form, hence comboboxes cannot expand), to prevent executing operations of the Qml document whose logic is unknown to linguist. - Qml item "ApplicationWindow" not supported: It needs a whole QQmlApplicationEngine. A normal QQuickWidget cannot contain it. - Missing Qml plugins: if some required Qml plugins are missing, the document cannot be displayed. This patch contains some changes in the cmake file to add the Qml plugins in QtDeclarative as dependencies, so most used plugins are supported. If due to the limitations above (or other limitations), a document cannot be viewed reasonably, the user has always the choice to fall back to the source code by toggling views->Preview Qml (then it will be same as before). Additionally, if there are some errors in showing the Qml document (e.g., due to missing Qml plugins), the editor automatically falls back to the source code. Fixes: QTBUG-71161 Change-Id: I3dc4ac741b8be31ef96fd5bd9f5a13e1251a8ce1 Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
1 parent d742ac2 commit bd736f3

10 files changed

+271
-33
lines changed

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ endif()
2121
find_package(Qt6 ${PROJECT_VERSION} CONFIG REQUIRED COMPONENTS BuildInternals Core)
2222
find_package(Qt6 ${PROJECT_VERSION} QUIET CONFIG OPTIONAL_COMPONENTS
2323
DBus Network Xml Widgets Quick QuickWidgets Qml QmlLSPrivate
24-
Sql PrintSupport OpenGL OpenGLWidgets ${optional_components})
24+
Sql PrintSupport OpenGL OpenGLWidgets QuickLayouts ${optional_components})
2525
qt_internal_project_setup()
2626

2727
qt_build_repo()

src/linguist/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ add_subdirectory(lupdate)
1616
add_subdirectory(lupdate-pro)
1717
if(QT_FEATURE_process AND QT_FEATURE_pushbutton AND QT_FEATURE_toolbutton
1818
AND QT_FEATURE_png AND QT_FEATURE_mdiarea AND QT_FEATURE_syntaxhighlighter
19-
AND TARGET Qt::Widgets)
19+
AND TARGET Qt::Widgets AND TARGET Qt::Quick)
2020
add_subdirectory(linguist)
2121
endif()
2222

src/linguist/linguist/CMakeLists.txt

+46-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ qt_internal_add_app(linguist
2121
batchtranslationdialog.cpp batchtranslationdialog.h
2222
errorsview.cpp errorsview.h
2323
finddialog.cpp finddialog.h finddialog.ui
24-
formpreviewview.cpp formpreviewview.h
24+
uiformpreviewview.cpp uiformpreviewview.h
25+
qmlformpreviewview.h qmlformpreviewview.cpp
2526
globals.cpp globals.h
2627
main.cpp
2728
mainwindow.cpp mainwindow.h mainwindow.ui
@@ -52,6 +53,8 @@ qt_internal_add_app(linguist
5253
Qt::GuiPrivate
5354
Qt::UiToolsPrivate
5455
Qt::Widgets
56+
Qt::QuickWidgets
57+
Qt::QuickLayouts
5558
)
5659

5760
qt_internal_extend_target(linguist CONDITION QT_FEATURE_printsupport
@@ -61,6 +64,48 @@ qt_internal_extend_target(linguist CONDITION QT_FEATURE_printsupport
6164
Qt::PrintSupport
6265
)
6366

67+
set(available_plugins "")
68+
69+
set(plugin_targets
70+
Qt::qtquickcontrols2plugin
71+
Qt::qtquick2plugin
72+
Qt::qtquickdialogsplugin
73+
Qt::qtquickcontrols2universalstyleplugin
74+
Qt::qtquickcontrols2basicstyleplugin
75+
Qt::qtquickcontrols2fluentwinui3styleplugin
76+
Qt::qtquickcontrols2fusionstyleplugin
77+
Qt::qtquickcontrols2imaginestyleplugin
78+
Qt::qtquickcontrols2iosstyleplugin
79+
Qt::qtquickcontrols2macosstyleplugin
80+
Qt::qtquickcontrols2materialstyleplugin
81+
Qt::qtquicktemplates2plugin
82+
Qt::qtquickcontrols2implplugin
83+
Qt::qtquickdialogs2quickimplplugin
84+
Qt::qtquickcontrols2universalstyleimplplugin
85+
Qt::qtquickcontrols2basicstyleimplplugin
86+
Qt::qtquickcontrols2fluentwinui3styleimplplugin
87+
Qt::qtquickcontrols2fusionstyleimplplugin
88+
Qt::qtquickcontrols2imaginestyleimplplugin
89+
Qt::qtquickcontrols2iosstyleimplplugin
90+
Qt::qtquickcontrols2macosstyleimplplugin
91+
Qt::qtquickcontrols2materialstyleimplplugin
92+
Qt::quickwindow
93+
)
94+
95+
foreach(plugin ${plugin_targets})
96+
if(TARGET ${plugin})
97+
list(APPEND available_plugins ${plugin})
98+
endif()
99+
endforeach()
100+
101+
if (available_plugins)
102+
if(BUILD_SHARED_LIBS)
103+
add_dependencies(linguist ${available_plugins})
104+
else()
105+
qt_internal_extend_target(linguist LIBRARIES ${available_plugins})
106+
endif()
107+
endif()
108+
64109
qt_add_ui(linguist
65110
SOURCES
66111
batchtranslation.ui

src/linguist/linguist/mainwindow.cpp

+47-18
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
#include "batchtranslationdialog.h"
1212
#include "errorsview.h"
1313
#include "finddialog.h"
14-
#include "formpreviewview.h"
14+
#include "uiformpreviewview.h"
15+
#include "qmlformpreviewview.h"
1516
#include "globals.h"
1617
#include "messageeditor.h"
1718
#include "messagemodel.h"
@@ -73,6 +74,16 @@ enum Ending {
7374
End_Ellipsis
7475
};
7576

77+
static bool hasUiFormPreview(const QString &fileName)
78+
{
79+
return fileName.endsWith(".ui"_L1) || fileName.endsWith(".jui"_L1);
80+
}
81+
82+
static bool hasQmlFormPreview(const QString &fileName, bool qmlPreviewChecked)
83+
{
84+
return fileName.endsWith(QLatin1String(".qml")) && qmlPreviewChecked;
85+
}
86+
7687
static QString leadingWhitespace(const QString &str)
7788
{
7889
int i = 0;
@@ -299,11 +310,6 @@ struct Validator
299310

300311
static const int MessageMS = 2500;
301312

302-
static bool hasFormPreview(const QString &fileName)
303-
{
304-
return fileName.endsWith(".ui"_L1) || fileName.endsWith(".jui"_L1);
305-
}
306-
307313
} // namespace
308314

309315
QT_BEGIN_NAMESPACE
@@ -484,10 +490,12 @@ MainWindow::MainWindow()
484490
m_sourceAndFormDock->setWindowTitle(tr("Sources and Forms"));
485491
m_sourceAndFormView = new QStackedWidget(this);
486492
m_sourceAndFormDock->setWidget(m_sourceAndFormView);
487-
m_formPreviewView = new FormPreviewView(0, m_dataModel);
493+
m_uiFormPreviewView = new UiFormPreviewView(0, m_dataModel);
494+
m_qmlFormPreviewView = new QmlFormPreviewView(m_dataModel);
488495
m_sourceCodeView = new SourceCodeView(0);
489496
m_sourceAndFormView->addWidget(m_sourceCodeView);
490-
m_sourceAndFormView->addWidget(m_formPreviewView);
497+
m_sourceAndFormView->addWidget(m_uiFormPreviewView);
498+
m_sourceAndFormView->addWidget(m_qmlFormPreviewView);
491499

492500
// Set up errors dock widget
493501
m_errorsDock = new QDockWidget(this);
@@ -684,7 +692,8 @@ void MainWindow::modelCountChanged()
684692
m_ui.actionFindNext->setEnabled(false);
685693
m_ui.actionFindPrev->setEnabled(false);
686694

687-
m_formPreviewView->setSourceContext(-1, 0);
695+
m_uiFormPreviewView->setSourceContext(-1, 0);
696+
m_qmlFormPreviewView->setSourceContext(-1, 0);
688697
}
689698

690699
struct OpenedFile {
@@ -1662,8 +1671,11 @@ void MainWindow::translationChanged(const MultiDataIndex &index)
16621671
updateDanger(index, true);
16631672

16641673
MessageItem *m = m_dataModel->messageItem(index);
1665-
if (hasFormPreview(m->fileName()))
1666-
m_formPreviewView->setSourceContext(index.model(), m);
1674+
if (hasUiFormPreview(m->fileName()))
1675+
m_uiFormPreviewView->setSourceContext(index.model(), m);
1676+
else if (hasQmlFormPreview(m->fileName(), m_ui.actionQmlPreview->isChecked()))
1677+
if (!m_qmlFormPreviewView->setSourceContext(index.model(), m))
1678+
m_ui.actionQmlPreview->setChecked(false);
16671679
}
16681680

16691681
// This and the following function operate directly on the messageitem,
@@ -1677,8 +1689,12 @@ void MainWindow::updateTranslation(const QStringList &translations)
16771689
return;
16781690

16791691
m->setTranslations(translations);
1680-
if (!m->fileName().isEmpty() && hasFormPreview(m->fileName()))
1681-
m_formPreviewView->setSourceContext(m_currentIndex.model(), m);
1692+
if (!m->fileName().isEmpty() && hasUiFormPreview(m->fileName()))
1693+
m_uiFormPreviewView->setSourceContext(m_currentIndex.model(), m);
1694+
else if (!m->fileName().isEmpty()
1695+
&& hasQmlFormPreview(m->fileName(), m_ui.actionQmlPreview->isChecked()))
1696+
if (!m_qmlFormPreviewView->setSourceContext(m_currentIndex.model(), m))
1697+
m_ui.actionQmlPreview->setChecked(false);
16821698
updateDanger(m_currentIndex, true);
16831699

16841700
if (m->isFinished())
@@ -2083,8 +2099,9 @@ void MainWindow::setupMenuBar()
20832099
connect(m_ui.actionDisplayGuesses, &QAction::triggered,
20842100
m_phraseView, &PhraseView::toggleGuessing);
20852101
connect(m_ui.actionStatistics, &QAction::triggered, this, &MainWindow::showStatistics);
2086-
connect(m_ui.actionVisualizeWhitespace, &QAction::triggered, this,
2087-
&MainWindow::toggleVisualizeWhitespace);
2102+
connect(m_ui.actionQmlPreview, &QAction::triggered, this, &MainWindow::toggleQmlPreview);
2103+
connect(m_ui.actionVisualizeWhitespace, &QAction::triggered,
2104+
this, &MainWindow::toggleVisualizeWhitespace);
20882105
connect(m_ui.actionIncreaseZoom, &QAction::triggered,
20892106
m_messageEditor, &MessageEditor::increaseFontSize);
20902107
connect(m_ui.actionDecreaseZoom, &QAction::triggered,
@@ -2189,10 +2206,14 @@ void MainWindow::doUpdateLatestModel(int model)
21892206
void MainWindow::updateSourceView(int model, MessageItem *item)
21902207
{
21912208
if (item && !item->fileName().isEmpty()) {
2192-
if (hasFormPreview(item->fileName())) {
2193-
m_sourceAndFormView->setCurrentWidget(m_formPreviewView);
2194-
m_formPreviewView->setSourceContext(model, item);
2209+
if (hasUiFormPreview(item->fileName())) {
2210+
m_sourceAndFormView->setCurrentWidget(m_uiFormPreviewView);
2211+
m_uiFormPreviewView->setSourceContext(model, item);
2212+
} else if (hasQmlFormPreview(item->fileName(), m_ui.actionQmlPreview->isChecked())
2213+
&& m_qmlFormPreviewView->setSourceContext(model, item)) {
2214+
m_sourceAndFormView->setCurrentWidget(m_qmlFormPreviewView);
21952215
} else {
2216+
m_ui.actionQmlPreview->setChecked(false);
21962217
m_sourceAndFormView->setCurrentWidget(m_sourceCodeView);
21972218
QDir dir = QFileInfo(m_dataModel->srcFileName(model)).dir();
21982219
QString fileName = QDir::cleanPath(dir.absoluteFilePath(item->fileName()));
@@ -2733,6 +2754,14 @@ void MainWindow::showStatistics()
27332754
updateStatistics();
27342755
}
27352756

2757+
void MainWindow::toggleQmlPreview()
2758+
{
2759+
if (m_ui.actionQmlPreview->isChecked())
2760+
m_sourceAndFormView->setCurrentWidget(m_qmlFormPreviewView);
2761+
else
2762+
m_sourceAndFormView->setCurrentWidget(m_sourceCodeView);
2763+
}
2764+
27362765
void MainWindow::toggleVisualizeWhitespace()
27372766
{
27382767
m_messageEditor->setVisualizeWhitespace(m_ui.actionVisualizeWhitespace->isChecked());

src/linguist/linguist/mainwindow.h

+5-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class QTreeView;
3636
class BatchTranslationDialog;
3737
class ErrorsView;
3838
class FocusWatcher;
39-
class FormPreviewView;
39+
class UiFormPreviewView;
40+
class QmlFormPreviewView;
4041
class MessageEditor;
4142
class PhraseView;
4243
class SourceCodeView;
@@ -129,6 +130,7 @@ private slots:
129130
FindDialog::FindOptions options, int statusFilter);
130131
void revalidate();
131132
void showStatistics();
133+
void toggleQmlPreview();
132134
void toggleVisualizeWhitespace();
133135
void onWhatsThis();
134136
void updatePhraseDicts();
@@ -193,7 +195,8 @@ private slots:
193195
PhraseView *m_phraseView;
194196
QStackedWidget *m_sourceAndFormView;
195197
SourceCodeView *m_sourceCodeView;
196-
FormPreviewView *m_formPreviewView;
198+
UiFormPreviewView *m_uiFormPreviewView;
199+
QmlFormPreviewView *m_qmlFormPreviewView;
197200
ErrorsView *m_errorsView;
198201
QLabel *m_progressLabel;
199202
QLabel *m_modifiedLabel;

src/linguist/linguist/mainwindow.ui

+18
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
</widget>
9898
<addaction name="actionResetSorting"/>
9999
<addaction name="actionDisplayGuesses"/>
100+
<addaction name="actionQmlPreview"/>
100101
<addaction name="actionLengthVariants"/>
101102
<addaction name="actionVisualizeWhitespace"/>
102103
<addaction name="separator"/>
@@ -645,6 +646,23 @@
645646
<enum>QAction::NoRole</enum>
646647
</property>
647648
</action>
649+
<action name="actionQmlPreview">
650+
<property name="checkable">
651+
<bool>true</bool>
652+
</property>
653+
<property name="checked">
654+
<bool>true</bool>
655+
</property>
656+
<property name="text">
657+
<string>&amp;QML preview</string>
658+
</property>
659+
<property name="whatsThis">
660+
<string>Display a preview of QML documents.</string>
661+
</property>
662+
<property name="menuRole">
663+
<enum>QAction::NoRole</enum>
664+
</property>
665+
</action>
648666
<action name="actionManual">
649667
<property name="text">
650668
<string>&amp;Manual</string>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (C) 2025 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 "qmlformpreviewview.h"
5+
6+
#include "messagemodel.h"
7+
8+
#include <QDir>
9+
#include <QQuickWidget>
10+
#include <QApplication>
11+
#include <QVBoxLayout>
12+
#include <QQuickItem>
13+
#include <QHash>
14+
#include <QMessageBox>
15+
16+
using namespace Qt::Literals::StringLiterals;
17+
18+
namespace {
19+
20+
void traverseQml(QObject *obj, QHash<QString, QList<QObject *>> &targets)
21+
{
22+
if (obj) {
23+
if (QVariant text = obj->property("text"); text.isValid()) {
24+
if (auto itr = targets.find(text.toString()); itr != targets.end()) {
25+
itr->append(obj);
26+
return;
27+
}
28+
}
29+
for (QObject *child : obj->children())
30+
traverseQml(child, targets);
31+
}
32+
}
33+
34+
void matchSources(QQuickItem *root, QHash<QString, QList<QObject *>> &targets)
35+
{
36+
traverseQml(root, targets);
37+
}
38+
39+
ContextItem *getContext(const DataModel *m, const QString &contextName)
40+
{
41+
for (int i = 0; i < m->contextCount(); i++)
42+
if (auto ctx = m->contextItem(i); ctx->context() == contextName)
43+
return ctx;
44+
Q_UNREACHABLE_RETURN(nullptr);
45+
}
46+
47+
QHash<QString, QList<QObject *>> extractSources(const DataModel *m, const QString &contextName)
48+
{
49+
QHash<QString, QList<QObject *>> t;
50+
ContextItem *ctx = getContext(m, contextName);
51+
for (int j = 0; j < ctx->messageCount(); j++)
52+
t[ctx->messageItem(j)->text()] = {};
53+
return t;
54+
}
55+
} // namespace
56+
57+
QT_BEGIN_NAMESPACE
58+
59+
QmlFormPreviewView::QmlFormPreviewView(MultiDataModel *dataModel)
60+
: QQuickWidget(0), m_dataModel(dataModel) {}
61+
62+
bool QmlFormPreviewView::setSourceContext(int model, MessageItem *messageItem)
63+
{
64+
if (model < 0 || !messageItem) {
65+
m_lastModel = -1;
66+
return true;
67+
}
68+
const QDir dir = QFileInfo(m_dataModel->srcFileName(model)).dir();
69+
const QString fileName = QDir::cleanPath(dir.absoluteFilePath(messageItem->fileName()));
70+
if (m_lastFormName != fileName) {
71+
m_lastFormName = fileName;
72+
73+
setAttribute(Qt::WA_TransparentForMouseEvents, !fileName.endsWith(".ui.qml"_L1));
74+
75+
setSource(QUrl::fromLocalFile(fileName));
76+
if (!errors().empty()) {
77+
QString errs;
78+
for (const auto &error : errors())
79+
errs += error.toString() + "\n"_L1;
80+
QMessageBox::warning(this, tr("Qt Linguist"),
81+
tr("Error loading QML file: %1").arg(errs));
82+
m_lastError = true;
83+
return false;
84+
}
85+
86+
m_targets = extractSources(m_dataModel->model(model), messageItem->context());
87+
matchSources(rootObject(), m_targets);
88+
89+
setResizeMode(SizeViewToRootObject);
90+
show();
91+
} else if (m_lastError) {
92+
return false;
93+
}
94+
if (m_lastModel != model) {
95+
ContextItem *ctx = getContext(m_dataModel->model(model), messageItem->context());
96+
for (int i = 0; i < ctx->messageCount(); i++) {
97+
MessageItem *message = ctx->messageItem(i);
98+
for (QObject *item : std::as_const(m_targets[message->text()]))
99+
item->setProperty("text", message->translation());
100+
}
101+
m_lastModel = model;
102+
} else {
103+
for (QObject *item : std::as_const(m_targets[messageItem->text()]))
104+
item->setProperty("text", messageItem->translation());
105+
}
106+
m_lastError = false;
107+
return true;
108+
}
109+
110+
QT_END_NAMESPACE

0 commit comments

Comments
 (0)