1 /* This file is part of the KDE project
2    Copyright (C) 2016 Jarosław Staniek <staniek@kde.org>
3 
4    This library is free software; you can redistribute it and/or
5    modify it under the terms of the GNU Library General Public
6    License as published by the Free Software Foundation; either
7    version 2 of the License, or (at your option) any later version.
8 
9    This library is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12    Library General Public License for more details.
13 
14    You should have received a copy of the GNU Library General Public License
15    along with this library; see the file COPYING.LIB.  If not, write to
16    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17  * Boston, MA 02110-1301, USA.
18 */
19 
20 #include <QCoreApplication>
21 #include <QDir>
22 #include <QFileInfo>
23 #include <QRegularExpression>
24 #include <QResource>
25 #include <QStandardPaths>
26 #include <QDebug>
27 
28 #ifdef QT_ONLY
29 #define KLocalizedString QString
30 #else
31 #include <KConfigGroup>
32 #include <KSharedConfig>
33 #include <KMessageBox>
34 #endif
35 
36 //! @todo Support other themes
37 const QString supportedIconTheme = QLatin1String("breeze");
38 
39 //! @return true if @a path is readable
fileReadable(const QString & path)40 static bool fileReadable(const QString &path)
41 {
42     return !path.isEmpty() && QFileInfo(path).isReadable();
43 }
44 
45 #ifdef Q_OS_WIN
46 #define KPATH_SEPARATOR ';'
47 #else
48 #define KPATH_SEPARATOR ':'
49 #endif
50 
51 //! @brief Used for a workaround: locations for QStandardPaths::AppDataLocation end with app name.
52 //! If this is not an expected app but for example a test app, replace
53 //! the subdir name with app name so we can find resource file(s).
correctStandardLocations(const QString & privateName,QStandardPaths::StandardLocation location,const QString & extraLocation)54 static QStringList correctStandardLocations(const QString &privateName,
55                                      QStandardPaths::StandardLocation location,
56                                      const QString &extraLocation)
57 {
58     QStringList result;
59     QStringList standardLocations(QStandardPaths::standardLocations(location));
60     if (!extraLocation.isEmpty()) {
61         standardLocations.append(extraLocation);
62     }
63     if (privateName.isEmpty()) {
64         result = standardLocations;
65     } else {
66         QRegularExpression re(QLatin1Char('/') + QCoreApplication::applicationName() + QLatin1Char('$'));
67         for(const QString &dir : standardLocations) {
68             if (dir.indexOf(re) != -1) {
69                 QString realDir(dir);
70                 realDir.replace(re, QLatin1Char('/') + privateName);
71                 result.append(realDir);
72             }
73         }
74     }
75     return result;
76 }
77 
locateFile(const QString & privateName,const QString & path,QStandardPaths::StandardLocation location,const QString & extraLocation,QStringList * triedLocations)78 static QString locateFile(const QString &privateName,
79                           const QString& path, QStandardPaths::StandardLocation location,
80                           const QString &extraLocation, QStringList *triedLocations)
81 {
82     Q_ASSERT(triedLocations);
83     const QString subdirPath = QFileInfo(path).dir().path();
84     {
85         // Priority #1: This makes the app portable and working without installation, from the build dir
86         const QString dataDir = QCoreApplication::applicationDirPath() + QStringLiteral("/data/") + privateName;
87         triedLocations->append(QDir::cleanPath(dataDir + '/' + subdirPath));
88         const QString dataFile = QFileInfo(dataDir + '/' + path).canonicalFilePath();
89         if (fileReadable(dataFile)) {
90             return dataFile;
91         }
92     }
93     // Priority #2: Let QStandardPaths handle this, it will look for app local stuff
94     const QStringList correctedStandardLocations(correctStandardLocations(privateName, location, extraLocation));
95     for (const QString &dir : correctedStandardLocations) {
96         triedLocations->append(QDir::cleanPath(dir + '/' + subdirPath));
97         const QString dataFile = QFileInfo(dir + QLatin1Char('/') + path).canonicalFilePath();
98         if (fileReadable(dataFile)) {
99             return dataFile;
100         }
101     }
102     // Priority #3: Try in PATH subdirs, useful for running apps from the build dir, without installing
103     for(const QByteArray &pathDir : qgetenv("PATH").split(KPATH_SEPARATOR)) {
104         const QString dataDir = QFile::decodeName(pathDir) + QStringLiteral("/data/");
105         triedLocations->append(QDir::cleanPath(dataDir + '/' + subdirPath));
106         const QString dataDirFromPath = QFileInfo(dataDir + '/' + path).canonicalFilePath();
107         if (fileReadable(dataDirFromPath)) {
108             return dataDirFromPath;
109         }
110     }
111     return QString();
112 }
113 
114 #ifndef KEXI_SKIP_REGISTERRESOURCE
115 
116 #ifdef KEXI_BASE_PATH
117 const QString BASE_PATH(KEXI_BASE_PATH);
118 #else
119 const QString BASE_PATH(QCoreApplication::applicationName());
120 #endif
121 
registerResource(const QString & path,QStandardPaths::StandardLocation location,const QString & resourceRoot,const QString & extraLocation,KLocalizedString * errorMessage,KLocalizedString * detailsErrorMessage)122 static bool registerResource(const QString& path, QStandardPaths::StandardLocation location,
123                              const QString &resourceRoot, const QString &extraLocation,
124                              KLocalizedString *errorMessage, KLocalizedString *detailsErrorMessage)
125 {
126     QStringList triedLocations;
127     const QString privateName = location == QStandardPaths::GenericDataLocation
128         ? QString() : BASE_PATH;
129     const QString fullPath = locateFile(privateName, path, location, extraLocation, &triedLocations);
130     if (fullPath.isEmpty()
131         || !QResource::registerResource(fullPath, resourceRoot))
132     {
133         const QString triedLocationsString = QLocale().createSeparatedList(triedLocations);
134 #ifdef QT_ONLY
135         *errorMessage = QString("Could not open icon resource file %1.").arg(path);
136         *detailsErrorMessage = QString("Tried to find in %1.").arg(triedLocationsString);
137 #else
138         *errorMessage = kxi18nc("@info",
139             "<para>Could not open icon resource file <filename>%1</filename>.</para>"
140             "<para><application>Kexi</application> will not start. "
141             "Please check if <application>Kexi</application> is properly installed.</para>")
142             .subs(QFileInfo(path).fileName());
143         *detailsErrorMessage = kxi18nc("@info Tried to find files in <dir list>",
144                                        "Tried to find in %1.").subs(triedLocationsString);
145 #endif
146         return false;
147     }
148     *errorMessage = KLocalizedString();
149     *detailsErrorMessage = KLocalizedString();
150     return true;
151 }
152 #endif // !KEXI_SKIP_REGISTERRESOURCE
153 
154 #ifndef KEXI_SKIP_SETUPBREEZEICONTHEME
155 
registerGlobalBreezeIconsResource(KLocalizedString * errorMessage,KLocalizedString * detailsErrorMessage)156 inline bool registerGlobalBreezeIconsResource(KLocalizedString *errorMessage,
157                                               KLocalizedString *detailsErrorMessage)
158 {
159     QString extraLocation;
160 #ifdef CMAKE_INSTALL_FULL_ICONDIR
161     extraLocation = QDir::fromNativeSeparators(QFile::decodeName(CMAKE_INSTALL_FULL_ICONDIR));
162     if (extraLocation.endsWith("/icons")) {
163         extraLocation.chop(QLatin1String("/icons").size());
164     }
165 #endif
166     return registerResource("icons/breeze/breeze-icons.rcc", QStandardPaths::GenericDataLocation,
167                             QStringLiteral("/icons/breeze"), extraLocation, errorMessage,
168                             detailsErrorMessage);
169 }
170 
171 //! Tell Qt about the theme
setupBreezeIconTheme()172 inline void setupBreezeIconTheme()
173 {
174 #ifdef QT_GUI_LIB
175     QIcon::setThemeSearchPaths(QStringList() << QStringLiteral(":/icons"));
176     QIcon::setThemeName(QStringLiteral("breeze"));
177 #endif
178 }
179 #endif // !KEXI_SETUPBREEZEICONTHEME
180 
181 #ifndef KEXI_SKIP_REGISTERICONSRESOURCE
182 /*! @brief Registers icons resource file
183  * @param privateName Name to be used instead of application name for resource lookup
184  * @param path Relative path to the resource file
185  * @param location Standard file location to use for file lookup
186  * @param resourceRoot A resource root for QResource::registerResource()
187  * @param errorMessage On failure it is set to a brief error message.
188  * @param errorDescription On failure it is set to a detailed error message.
189  * other for warning
190  */
registerIconsResource(const QString & privateName,const QString & path,QStandardPaths::StandardLocation location,const QString & resourceRoot,const QString & extraLocation,QString * errorMessage,QString * detailedErrorMessage)191 static bool registerIconsResource(const QString &privateName, const QString& path,
192                              QStandardPaths::StandardLocation location,
193                              const QString &resourceRoot, const QString &extraLocation,
194                              QString *errorMessage, QString *detailedErrorMessage)
195 {
196     QStringList triedLocations;
197     const QString fullPath = locateFile(privateName, path, location, extraLocation, &triedLocations);
198     if (fullPath.isEmpty() || !QFileInfo(fullPath).isReadable()
199         || !QResource::registerResource(fullPath, resourceRoot))
200     {
201         const QString triedLocationsString = QLocale().createSeparatedList(triedLocations);
202 #ifdef QT_ONLY
203         *errorMessage
204             = QString("Could not open icon resource file \"%1\". Please check if application "
205                       "is properly installed.").arg(path);
206         *detailedErrorMessage = QString("Tried to find in %1.").arg(triedLocationsString);
207 #else
208         *errorMessage = xi18nc(
209             "@info", "Could not open icon resource file <filename>%1</filename>. "
210                      "Please check if <application>%2</application> is properly installed.",
211             QFileInfo(path).fileName(), QApplication::applicationDisplayName());
212         *detailedErrorMessage = xi18nc("@info Tried to find files in <dir list>",
213                                        "Tried to find in %1.", triedLocationsString);
214 #endif
215         return false;
216     }
217     *errorMessage = QString();
218     *detailedErrorMessage = QString();
219     return true;
220 }
221 #endif // !KEXI_SKIP_SETUPBREEZEICONTHEME
222 
223 #if !defined QT_ONLY  && !defined KEXI_SKIP_SETUPPRIVATEICONSRESOURCE
224 /*! @brief Sets up a private icon resource file
225  * @return @c false on failure and sets error message. Does not warn or exit on failure.
226  * @param privateName Name to be used instead of application name for resource lookup
227  * @param path Relative path to the resource file
228  * @param themeName Icon theme to use. It affects filename.
229  * @param errorMessage On failure it is set to a brief error message
230  * @param errorDescription On failure it is set to a detailed error message
231  * other for warning
232  * @param prefix Resource path prefix. The default is useful for library-global resource,
233  * other values is useful for plugins.
234  */
235 static bool setupPrivateIconsResource(const QString &privateName, const QString& path,
236                                const QString &themeName,
237                                QString *errorMessage, QString *detailedErrorMessage,
238                                const QString &prefix = QLatin1String(":/icons"))
239 {
240     // Register application's resource first to have priority over the theme.
241     // Some icons may exists in both resources.
242     if (!registerIconsResource(privateName, path,
243                           QStandardPaths::AppDataLocation,
244                           QString(), QString(), errorMessage, detailedErrorMessage))
245     {
246         return false;
247     }
248     bool changeTheme = false;
249 #ifdef QT_GUI_LIB
250     QIcon::setThemeSearchPaths(QStringList() << prefix << QIcon::themeSearchPaths());
251     changeTheme = 0 != QIcon::themeName().compare(themeName, Qt::CaseInsensitive);
252     if (changeTheme) {
253         QIcon::setThemeName(themeName);
254     }
255 #endif
256 
257     KConfigGroup cg(KSharedConfig::openConfig(), "Icons");
258     changeTheme = changeTheme || 0 != cg.readEntry("Theme", QString()).compare(themeName, Qt::CaseInsensitive);
259     // tell KIconLoader an co. about the theme
260     if (changeTheme) {
261         cg.writeEntry("Theme", themeName);
262         cg.sync();
263     }
264     return true;
265 }
266 
267 /*! @brief Sets up a private icon resource file
268  * @return @c false on failure and sets error message.
269  * @param privateName Name to be used instead of application name for resource lookup
270  * @param path Relative path to the resource file
271  * @param themeName Icon theme to use. It affects filename.
272  * @param errorMessage On failure it is set to a brief error message.
273  * @param errorDescription On failure it is set to a detailed error message.
274  * other for warning
275  * @param prefix Resource path prefix. The default is useful for library-global resource,
276  * other values is useful for plugins.
277  */
278 static bool setupPrivateIconsResourceWithMessage(const QString &privateName, const QString& path,
279                                           const QString &themeName,
280                                           QString *errorMessage, QString *detailedErrorMessage,
281                                           const QString &prefix = QLatin1String(":/icons"))
282 {
283     if (!setupPrivateIconsResource(privateName, path, themeName,
284                                    errorMessage, detailedErrorMessage, prefix))
285     {
286         if (detailedErrorMessage->isEmpty()) {
287             KMessageBox::error(nullptr, *errorMessage);
288         } else {
289             KMessageBox::detailedError(nullptr, *errorMessage, *detailedErrorMessage);
290         }
291         return false;
292     }
293     return true;
294 }
295 
296 /*! @overload setupPrivateIconsResourceWithMessage(QString &privateName, const QString& path,
297                                           const QString &themeName,
298                                           QString *errorMessage, QString *detailedErrorMessage,
299                                           const QString &prefix = QLatin1String(":/icons"))
300     Uses default theme name.
301  */
302 static bool setupPrivateIconsResourceWithMessage(const QString &privateName, const QString& path,
303                                           QString *errorMessage, QString *detailedErrorMessage,
304                                           const QString &prefix = QLatin1String(":/icons"))
305 {
306     return setupPrivateIconsResourceWithMessage(privateName, path, supportedIconTheme,
307                                                 errorMessage, detailedErrorMessage, prefix);
308 }
309 
310 /*! @brief Sets up a private icon resource file
311  * Warns on failure and returns @c false.
312  * @param privateName Name to be used instead of application name for resource lookup
313  * @param path Relative path to the resource file
314  * @param messageType Type of message to use on error, QtFatalMsg for fatal exit and any
315  * other for warning
316  * @param prefix Resource path prefix. The default is useful for library-global resource,
317  * other values is useful for plugins.
318  */
319 static bool setupPrivateIconsResourceWithMessage(const QString &privateName, const QString& path,
320                                           QtMsgType messageType,
321                                           const QString &prefix = QLatin1String(":/icons"))
322 {
323     QString errorMessage;
324     QString detailedErrorMessage;
325     if (!setupPrivateIconsResourceWithMessage(privateName, path,
326                                               &errorMessage, &detailedErrorMessage, prefix)) {
327         if (messageType == QtFatalMsg) {
328             qFatal("%s %s", qPrintable(errorMessage), qPrintable(detailedErrorMessage));
329         } else {
330             qWarning() << qPrintable(errorMessage) << qPrintable(detailedErrorMessage);
331         }
332         return false;
333     }
334     return true;
335 }
336 
337 #endif // !QT_ONLY && !KEXI_SKIP_SETUPPRIVATEICONSRESOURCE
338