| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2016 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the test suite of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | |
| 30 | #include <QtTest/QtTest> |
| 31 | |
| 32 | #include <qapplication.h> |
| 33 | #include <private/qguiapplication_p.h> |
| 34 | #include <QtCore/QSet> |
| 35 | #include <QtCore/QFile> |
| 36 | #include <QtCore/QTranslator> |
| 37 | #include <QtCore/QTemporaryDir> |
| 38 | #include <private/qthread_p.h> |
| 39 | #include <qpa/qplatformtheme.h> |
| 40 | #include <QtWidgets/QInputDialog> |
| 41 | #include <QtWidgets/QColorDialog> |
| 42 | #include <QtWidgets/QDialogButtonBox> |
| 43 | #include <QtWidgets/QFileDialog> |
| 44 | #include <QtWidgets/QDesktopWidget> |
| 45 | |
| 46 | class tst_languageChange : public QObject |
| 47 | { |
| 48 | Q_OBJECT |
| 49 | public: |
| 50 | tst_languageChange(); |
| 51 | |
| 52 | public slots: |
| 53 | void initTestCase(); |
| 54 | void cleanupTestCase(); |
| 55 | private slots: |
| 56 | void retranslatability_data(); |
| 57 | void retranslatability(); |
| 58 | |
| 59 | private: |
| 60 | QDialogButtonBox::ButtonLayout m_layout; |
| 61 | }; |
| 62 | |
| 63 | |
| 64 | tst_languageChange::tst_languageChange() : |
| 65 | m_layout(QDialogButtonBox::WinLayout) |
| 66 | { |
| 67 | if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) |
| 68 | m_layout = static_cast<QDialogButtonBox::ButtonLayout>(theme->themeHint(hint: QPlatformTheme::DialogButtonBoxLayout).toInt()); |
| 69 | } |
| 70 | |
| 71 | void tst_languageChange::initTestCase() |
| 72 | { |
| 73 | } |
| 74 | |
| 75 | void tst_languageChange::cleanupTestCase() |
| 76 | { |
| 77 | } |
| 78 | /** |
| 79 | * Records all calls to translate() |
| 80 | */ |
| 81 | class TransformTranslator : public QTranslator |
| 82 | { |
| 83 | Q_OBJECT |
| 84 | public: |
| 85 | TransformTranslator() : QTranslator() {} |
| 86 | TransformTranslator(QObject *parent) : QTranslator(parent) {} |
| 87 | QString translate(const char *context, const char *sourceText, |
| 88 | const char *disambiguation = 0, int = -1) const |
| 89 | { |
| 90 | QByteArray total(context); |
| 91 | total.append(s: "::" ); |
| 92 | total.append(s: sourceText); |
| 93 | if (disambiguation) { |
| 94 | total.append(s: "::" ); |
| 95 | total.append(s: disambiguation); |
| 96 | } |
| 97 | m_translations.insert(value: total); |
| 98 | QString res; |
| 99 | for (int i = 0; i < int(qstrlen(str: sourceText)); ++i) { |
| 100 | QChar ch = QLatin1Char(sourceText[i]); |
| 101 | if (ch.isLower()) { |
| 102 | res.append(c: ch.toUpper()); |
| 103 | } else if (ch.isUpper()) { |
| 104 | res.append(c: ch.toLower()); |
| 105 | } else { |
| 106 | res.append(c: ch); |
| 107 | } |
| 108 | } |
| 109 | return res; |
| 110 | } |
| 111 | |
| 112 | virtual bool isEmpty() const { return false; } |
| 113 | |
| 114 | public: |
| 115 | mutable QSet<QByteArray> m_translations; |
| 116 | }; |
| 117 | |
| 118 | // Install the translator and close all application windows after a while to |
| 119 | // quit the event loop. |
| 120 | class LanguageTestStateMachine : public QObject |
| 121 | { |
| 122 | Q_OBJECT |
| 123 | public: |
| 124 | LanguageTestStateMachine(QTranslator *translator); |
| 125 | void start() { m_timer.start(); } |
| 126 | |
| 127 | private slots: |
| 128 | void timeout(); |
| 129 | |
| 130 | private: |
| 131 | enum State { InstallTranslator, CloseDialog }; |
| 132 | |
| 133 | QTimer m_timer; |
| 134 | QTranslator *m_translator; |
| 135 | State m_state; |
| 136 | }; |
| 137 | |
| 138 | LanguageTestStateMachine::LanguageTestStateMachine(QTranslator *translator) : |
| 139 | m_translator(translator), m_state(InstallTranslator) |
| 140 | { |
| 141 | m_timer.setInterval(500); |
| 142 | connect(sender: &m_timer, SIGNAL(timeout()), receiver: this, SLOT(timeout())); |
| 143 | } |
| 144 | |
| 145 | void LanguageTestStateMachine::timeout() |
| 146 | { |
| 147 | switch (m_state) { |
| 148 | case InstallTranslator: |
| 149 | m_timer.stop(); |
| 150 | QCoreApplication::installTranslator(messageFile: m_translator); |
| 151 | m_timer.setInterval(2500); |
| 152 | m_timer.start(); |
| 153 | m_state = CloseDialog; |
| 154 | break; |
| 155 | case CloseDialog: // Close repeatedly in case file dialog is slow. |
| 156 | QApplication::closeAllWindows(); |
| 157 | break; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | enum DialogType { |
| 162 | InputDialog = 1, |
| 163 | ColorDialog, |
| 164 | FileDialog |
| 165 | }; |
| 166 | |
| 167 | typedef QSet<QByteArray> TranslationSet; |
| 168 | Q_DECLARE_METATYPE(TranslationSet) |
| 169 | |
| 170 | void tst_languageChange::retranslatability_data() |
| 171 | { |
| 172 | QTest::addColumn<int>(name: "dialogType" ); |
| 173 | QTest::addColumn<TranslationSet >(name: "expected" ); |
| 174 | |
| 175 | //next we fill it with data |
| 176 | QTest::newRow( dataTag: "QInputDialog" ) |
| 177 | << int(InputDialog) << (QSet<QByteArray>() |
| 178 | << "QPlatformTheme::Cancel" ); |
| 179 | |
| 180 | QTest::newRow( dataTag: "QColorDialog" ) |
| 181 | << int(ColorDialog) << (QSet<QByteArray>() |
| 182 | << "QPlatformTheme::Cancel" |
| 183 | << "QColorDialog::&Sat:" |
| 184 | << "QColorDialog::&Add to Custom Colors" |
| 185 | << "QColorDialog::&Green:" |
| 186 | << "QColorDialog::&Red:" |
| 187 | << "QColorDialog::Bl&ue:" |
| 188 | << "QColorDialog::A&lpha channel:" |
| 189 | << "QColorDialog::&Basic colors" |
| 190 | << "QColorDialog::&Custom colors" |
| 191 | << "QColorDialog::&Val:" |
| 192 | << "QColorDialog::Hu&e:" ); |
| 193 | |
| 194 | QTest::newRow( dataTag: "QFileDialog" ) |
| 195 | << int(FileDialog) << (QSet<QByteArray>() |
| 196 | << "QFileDialog::All Files (*)" |
| 197 | << "QFileDialog::Back" |
| 198 | << "QFileDialog::Create New Folder" |
| 199 | << "QFileDialog::Detail View" |
| 200 | #if !defined(Q_OS_MAC) && !defined(Q_OS_WINRT) |
| 201 | << "QFileDialog::File" |
| 202 | #endif |
| 203 | << "QFileDialog::Files of type:" |
| 204 | << "QFileDialog::Forward" |
| 205 | << "QFileDialog::List View" |
| 206 | << "QFileDialog::Look in:" |
| 207 | << "QFileDialog::Open" |
| 208 | << "QFileDialog::Parent Directory" |
| 209 | << "QFileDialog::Show " |
| 210 | << "QFileDialog::Show &hidden files" |
| 211 | << "QFileDialog::&Delete" |
| 212 | << "QFileDialog::&New Folder" |
| 213 | << "QFileDialog::&Rename" |
| 214 | << "QFileSystemModel::Date Modified" |
| 215 | #ifdef Q_OS_WIN |
| 216 | << "QFileSystemModel::My Computer" |
| 217 | #else |
| 218 | << "QFileSystemModel::Computer" |
| 219 | #endif |
| 220 | << "QFileSystemModel::Size" |
| 221 | #ifdef Q_OS_MAC |
| 222 | << "QFileSystemModel::Kind::Match OS X Finder" |
| 223 | #else |
| 224 | << "QFileSystemModel::Type::All other platforms" |
| 225 | #endif |
| 226 | // << "QFileSystemModel::%1 KB" |
| 227 | << "QPlatformTheme::Cancel" |
| 228 | << "QPlatformTheme::Open" |
| 229 | << "QFileDialog::File &name:" ); |
| 230 | } |
| 231 | |
| 232 | void tst_languageChange::retranslatability() |
| 233 | { |
| 234 | QFETCH( int, dialogType); |
| 235 | QFETCH( TranslationSet, expected); |
| 236 | |
| 237 | if (m_layout == QDialogButtonBox::GnomeLayout) |
| 238 | QSKIP("The input data are not suitable for this layout (QDialogButtonBox::GnomeLayout)" ); |
| 239 | |
| 240 | // This will always be queried for when a language changes |
| 241 | expected.insert(value: "QGuiApplication::QT_LAYOUT_DIRECTION::Translate this string to the string 'LTR' in left-to-right " |
| 242 | "languages or to 'RTL' in right-to-left languages (such as Hebrew and Arabic) to " |
| 243 | "get proper widget layout." ); |
| 244 | |
| 245 | TransformTranslator translator; |
| 246 | LanguageTestStateMachine stateMachine(&translator); |
| 247 | |
| 248 | switch (dialogType) { |
| 249 | case InputDialog: |
| 250 | stateMachine.start(); |
| 251 | QInputDialog::getInt(parent: 0, title: QLatin1String("title" ), label: QLatin1String("label" )); |
| 252 | break; |
| 253 | |
| 254 | case ColorDialog: |
| 255 | #ifdef Q_OS_MAC |
| 256 | QSKIP("The native color dialog is used on Mac OS" ); |
| 257 | #else |
| 258 | stateMachine.start(); |
| 259 | QColorDialog::getColor(); |
| 260 | #endif |
| 261 | break; |
| 262 | case FileDialog: { |
| 263 | #ifdef Q_OS_MAC |
| 264 | QSKIP("The native file dialog is used on Mac OS" ); |
| 265 | #endif |
| 266 | QFileDialog dlg; |
| 267 | dlg.setOption(option: QFileDialog::DontUseNativeDialog); |
| 268 | QString tempDirPattern = QDir::tempPath(); |
| 269 | if (!tempDirPattern.endsWith(c: QLatin1Char('/'))) |
| 270 | tempDirPattern += QLatin1Char('/'); |
| 271 | tempDirPattern += QStringLiteral("languagechangetestdirXXXXXX" ); |
| 272 | QTemporaryDir temporaryDir(tempDirPattern); |
| 273 | temporaryDir.setAutoRemove(true); |
| 274 | QVERIFY2(temporaryDir.isValid(), qPrintable(temporaryDir.errorString())); |
| 275 | const QString finalDir = temporaryDir.path() + QStringLiteral("/finaldir" ); |
| 276 | const QString fooName = temporaryDir.path() + QStringLiteral("/foo" ); |
| 277 | QDir dir; |
| 278 | QVERIFY(dir.mkpath(finalDir)); |
| 279 | QFile fooFile(fooName); |
| 280 | QVERIFY(fooFile.open(QIODevice::WriteOnly|QIODevice::Text)); |
| 281 | fooFile.write(data: "test" ); |
| 282 | fooFile.close(); |
| 283 | dlg.setDirectory(temporaryDir.path()); |
| 284 | dlg.setFileMode(QFileDialog::ExistingFiles); |
| 285 | dlg.setViewMode(QFileDialog::Detail); |
| 286 | stateMachine.start(); |
| 287 | dlg.exec(); |
| 288 | QTest::qWait(ms: 3000); |
| 289 | break; } |
| 290 | } |
| 291 | |
| 292 | // In case we use a Color dialog, we do not want to test for |
| 293 | // strings non existing in the dialog and which do not get |
| 294 | // translated. |
| 295 | const QSize desktopSize = QApplication::desktop()->size(); |
| 296 | if (dialogType == ColorDialog && (desktopSize.width() < 480 || desktopSize.height() < 350)) { |
| 297 | expected.remove(value: "QColorDialog::&Basic colors" ); |
| 298 | expected.remove(value: "QColorDialog::&Custom colors" ); |
| 299 | expected.remove(value: "QColorDialog::&Define Custom Colors >>" ); |
| 300 | expected.remove(value: "QColorDialog::&Add to Custom Colors" ); |
| 301 | } |
| 302 | |
| 303 | // see if all of our *expected* translations was translated. |
| 304 | // (There might be more, but thats not that bad) |
| 305 | QSet<QByteArray> commonTranslations = expected; |
| 306 | commonTranslations.intersect(other: translator.m_translations); |
| 307 | if (!expected.subtract(other: commonTranslations).isEmpty()) { |
| 308 | qDebug() << "Missing:" << expected; |
| 309 | if (!translator.m_translations.subtract(other: commonTranslations).isEmpty()) |
| 310 | qDebug() << "Unexpected:" << translator.m_translations; |
| 311 | } |
| 312 | |
| 313 | QVERIFY(expected.isEmpty()); |
| 314 | } |
| 315 | |
| 316 | QTEST_MAIN(tst_languageChange) |
| 317 | #include "tst_languagechange.moc" |
| 318 | |