1 /* GCompris - ApplicationInfo.cpp
2  *
3  * SPDX-FileCopyrightText: 2014-2015 Bruno Coudoin <bruno.coudoin@gcompris.net>
4  *
5  * Authors:
6  *   Bruno Coudoin <bruno.coudoin@gcompris.net>
7  *
8  * This file was originally created from Digia example code under BSD license
9  * and heavily modified since then.
10  *
11  *   SPDX-License-Identifier: GPL-3.0-or-later
12  */
13 
14 #include "ApplicationInfo.h"
15 
16 #include <QtQml>
17 #include <QtMath>
18 #include <QUrl>
19 #include <QUrlQuery>
20 #include <QGuiApplication>
21 #include <QScreen>
22 #include <QLocale>
23 #include <QQuickWindow>
24 #include <QStandardPaths>
25 #include <QSensor>
26 
27 #include <qmath.h>
28 #include <QDebug>
29 
30 #include <QFontDatabase>
31 #include <QDir>
32 
33 QQuickWindow *ApplicationInfo::m_window = nullptr;
34 ApplicationInfo *ApplicationInfo::m_instance = nullptr;
35 
ApplicationInfo(QObject * parent)36 ApplicationInfo::ApplicationInfo(QObject *parent) :
37     QObject(parent)
38 {
39 
40 #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_BLACKBERRY) || defined(SAILFISHOS) || defined(UBUNTUTOUCH)
41     m_isMobile = true;
42 #else
43     m_isMobile = false;
44 #endif
45 
46 #if defined(Q_OS_ANDROID)
47     // Put android before checking linux/unix as it is also a linux
48     m_platform = Android;
49 #elif defined(UBUNTUTOUCH)
50     m_platform = UbuntuTouchOS;
51 #elif defined(Q_OS_MAC)
52     m_platform = MacOSX;
53 #elif (defined(Q_OS_LINUX) || defined(Q_OS_UNIX))
54     m_platform = Linux;
55 #elif defined(Q_OS_WIN)
56     m_platform = Windows;
57 #elif defined(Q_OS_IOS)
58     m_platform = Ios;
59 #elif defined(Q_OS_BLACKBERRY)
60     m_platform = Blackberry;
61 #elif defined(SAILFISHOS)
62     m_platform = SailfishOS;
63 #else // default is Linux
64     m_platform = Linux;
65 #endif
66 
67     m_isBox2DInstalled = false;
68 
69     QRect rect = qApp->primaryScreen()->geometry();
70     m_ratio = qMin(qMax(rect.width(), rect.height()) / 800., qMin(rect.width(), rect.height()) / 520.);
71     // calculate a factor for font-scaling, cf.
72     // https://doc.qt.io/qt-5/scalability.html#calculating-scaling-ratio
73     qreal refDpi = 216.;
74     qreal refHeight = 1776.;
75     qreal refWidth = 1080.;
76     qreal height = qMax(rect.width(), rect.height());
77     qreal width = qMin(rect.width(), rect.height());
78     qreal dpi = qApp->primaryScreen()->logicalDotsPerInch();
79 
80 #if defined(UBUNTUTOUCH)
81     m_fontRatio = floor(m_ratio * 10) / 10;
82 #else
83     m_fontRatio = qMax(qreal(1.0), qMin(height * refDpi / (dpi * refHeight), width * refDpi / (dpi * refWidth)));
84 #endif
85     m_isPortraitMode = m_isMobile ? rect.height() > rect.width() : false;
86     m_applicationWidth = m_isMobile ? rect.width() : 1120;
87 
88     m_useOpenGL = true;
89 
90     if (m_isMobile)
91         connect(qApp->primaryScreen(), &QScreen::physicalSizeChanged, this, &ApplicationInfo::notifyPortraitMode);
92 
93 // @FIXME this does not work on iOS: https://bugreports.qt.io/browse/QTBUG-50624
94 #if !defined(Q_OS_IOS)
95     // Get all symbol fonts to remove them
96     QFontDatabase database;
97     m_excludedFonts = database.families(QFontDatabase::Symbol);
98 #endif
99     // Get fonts from rcc
100     const QStringList fontFilters = { "*.otf", "*.ttf" };
101     m_fontsFromRcc = QDir(":/gcompris/src/core/resource/fonts").entryList(fontFilters);
102 }
103 
~ApplicationInfo()104 ApplicationInfo::~ApplicationInfo()
105 {
106     m_instance = nullptr;
107 }
108 
sensorIsSupported(const QString & sensorType)109 bool ApplicationInfo::sensorIsSupported(const QString &sensorType)
110 {
111     return QSensor::sensorTypes().contains(sensorType.toUtf8());
112 }
113 
getNativeOrientation()114 Qt::ScreenOrientation ApplicationInfo::getNativeOrientation()
115 {
116     return QGuiApplication::primaryScreen()->nativeOrientation();
117 }
118 
setApplicationWidth(const int newWidth)119 void ApplicationInfo::setApplicationWidth(const int newWidth)
120 {
121     if (newWidth != m_applicationWidth) {
122         m_applicationWidth = newWidth;
123         emit applicationWidthChanged();
124     }
125 }
126 
getResourceDataPaths()127 QStringList ApplicationInfo::getResourceDataPaths()
128 {
129     return { ApplicationSettings::getInstance()->userDataPath(), "qrc:/gcompris/data" };
130 }
131 
getFilePath(const QString & file)132 QString ApplicationInfo::getFilePath(const QString &file)
133 {
134 #if defined(Q_OS_ANDROID)
135     return QString("assets:/share/GCompris/rcc/%1").arg(file);
136 #elif defined(Q_OS_MACX)
137     return QString("%1/../Resources/rcc/%2").arg(QCoreApplication::applicationDirPath(), file);
138 #elif defined(Q_OS_IOS)
139     return QString("%1/rcc/%2").arg(QCoreApplication::applicationDirPath(), file);
140 #else
141     return QString("%1/%2/rcc/%3").arg(QCoreApplication::applicationDirPath(), GCOMPRIS_DATA_FOLDER, file);
142 #endif
143 }
144 
getAudioFilePath(const QString & file)145 QString ApplicationInfo::getAudioFilePath(const QString &file)
146 {
147     QString localeName = getVoicesLocale(ApplicationSettings::getInstance()->locale());
148     return getAudioFilePathForLocale(file, localeName);
149 }
150 
getAudioFilePathForLocale(const QString & file,const QString & localeName)151 QString ApplicationInfo::getAudioFilePathForLocale(const QString &file,
152                                                    const QString &localeName)
153 {
154     QString filename = file;
155     filename.replace("$LOCALE", localeName);
156     filename.replace("$CA", CompressedAudio());
157     // Absolute paths are returned as is
158     if (file.startsWith('/') || file.startsWith(QLatin1String("qrc:")) || file.startsWith(':'))
159         return filename;
160 
161     // For relative paths, look into resource folders if found
162     const QStringList dataPaths = getResourceDataPaths();
163     for (const QString &dataPath: dataPaths) {
164         if (QFile::exists(dataPath + '/' + filename)) {
165             if (dataPath.startsWith('/'))
166                 return "file://" + dataPath + '/' + filename;
167             else
168                 return dataPath + '/' + filename;
169         }
170     }
171     // If not found, return the default qrc:/gcompris/data path
172     return dataPaths.last() + '/' + filename;
173 }
174 
getLocaleFilePath(const QString & file)175 QString ApplicationInfo::getLocaleFilePath(const QString &file)
176 {
177     QString localeShortName = localeShort();
178 
179     QString filename = file;
180     filename.replace("$LOCALE", localeShortName);
181     return filename;
182 }
183 
getSystemExcludedFonts()184 QStringList ApplicationInfo::getSystemExcludedFonts()
185 {
186     return m_excludedFonts;
187 }
188 
getFontsFromRcc()189 QStringList ApplicationInfo::getFontsFromRcc()
190 {
191     return m_fontsFromRcc;
192 }
193 
getBackgroundMusicFromRcc()194 QStringList ApplicationInfo::getBackgroundMusicFromRcc()
195 {
196     const QStringList backgroundMusicFilters = { QString("*.%1").arg(COMPRESSED_AUDIO) };
197     m_backgroundMusicFromRcc = QDir(":/gcompris/data/backgroundMusic").entryList(backgroundMusicFilters);
198     return m_backgroundMusicFromRcc;
199 }
200 
notifyPortraitMode()201 void ApplicationInfo::notifyPortraitMode()
202 {
203     int width = qApp->primaryScreen()->geometry().width();
204     int height = qApp->primaryScreen()->geometry().height();
205     setIsPortraitMode(height > width);
206 }
207 
setIsPortraitMode(const bool newMode)208 void ApplicationInfo::setIsPortraitMode(const bool newMode)
209 {
210     if (m_isPortraitMode != newMode) {
211         m_isPortraitMode = newMode;
212         emit portraitModeChanged();
213     }
214 }
215 
setWindow(QQuickWindow * window)216 void ApplicationInfo::setWindow(QQuickWindow *window)
217 {
218     m_window = window;
219 }
220 
screenshot(const QString & file)221 void ApplicationInfo::screenshot(const QString &file)
222 {
223     QImage img = m_window->grabWindow();
224     img.save(file);
225 }
226 
notifyFullscreenChanged()227 void ApplicationInfo::notifyFullscreenChanged()
228 {
229     if (ApplicationSettings::getInstance()->isFullscreen())
230         m_window->showFullScreen();
231     else
232         m_window->showNormal();
233 }
234 
235 // Would be better to create a component importing Box2D 2.0 using QQmlContext and test if it exists but it does not work.
setBox2DInstalled(const QQmlEngine & engine)236 void ApplicationInfo::setBox2DInstalled(const QQmlEngine &engine)
237 {
238     /*
239       QQmlContext *context = new QQmlContext(engine.rootContext());
240       context->setContextObject(&myDataSet);
241 
242       QQmlComponent component(&engine);
243       component.setData("import QtQuick 2.0\nimport Box2D 2.0\nItem { }", QUrl());
244       component.create(context);
245       box2dInstalled = (component != nullptr);
246     */
247     bool box2dInstalled = false;
248     for (const QString &folder: engine.importPathList()) {
249         if (QDir(folder).entryList().contains(QStringLiteral("Box2D.2.0"))) {
250             if (QDir(folder + "/Box2D.2.0").entryList().contains("qmldir")) {
251                 qDebug() << "Found box2d in " << folder;
252                 box2dInstalled = true;
253                 break;
254             }
255         }
256     }
257     m_isBox2DInstalled = box2dInstalled;
258     emit isBox2DInstalledChanged();
259 }
260 
261 // return the shortest possible locale name for the given locale, describing
262 // a unique voices dataset
getVoicesLocale(const QString & locale)263 QString ApplicationInfo::getVoicesLocale(const QString &locale)
264 {
265     QString _locale = locale;
266     if (_locale == GC_DEFAULT_LOCALE) {
267         _locale = QLocale::system().name();
268         if (_locale == "C")
269             _locale = "en_US";
270     }
271     // locales we have country-specific voices for:
272     /* clang-format off */
273     if (_locale.startsWith(QLatin1String("en_GB")) ||
274         _locale.startsWith(QLatin1String("en_US")) ||
275         _locale.startsWith(QLatin1String("pt_BR")) ||
276         _locale.startsWith(QLatin1String("zh_CN")) ||
277         _locale.startsWith(QLatin1String("zh_TW"))) {
278         return QLocale(_locale).name();
279     }
280     /* clang-format on */
281 
282     // short locale for all the rest:
283     return localeShort(_locale);
284 }
285 
localeSort(QVariantList list,const QString & locale) const286 QVariantList ApplicationInfo::localeSort(QVariantList list,
287                                          const QString &locale) const
288 {
289     std::sort(list.begin(), list.end(),
290               [&locale, this](const QVariant &a, const QVariant &b) {
291                   return (localeCompare(a.toString(), b.toString(), locale) < 0);
292               });
293     return list;
294 }
295 
applicationInfoProvider(QQmlEngine * engine,QJSEngine * scriptEngine)296 QObject *ApplicationInfo::applicationInfoProvider(QQmlEngine *engine,
297                                                   QJSEngine *scriptEngine)
298 {
299     Q_UNUSED(engine)
300     Q_UNUSED(scriptEngine)
301     /*
302      * Connect the fullscreen change signal to applicationInfo in order to change
303      * the QQuickWindow value
304      */
305     ApplicationInfo *appInfo = getInstance();
306     connect(ApplicationSettings::getInstance(), &ApplicationSettings::fullscreenChanged, appInfo,
307             &ApplicationInfo::notifyFullscreenChanged);
308 
309     return appInfo;
310 }
311