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