1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  * ----
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; see the file COPYING.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23 
24 #include "utils.h"
25 #include "config.h"
26 #include <QFile>
27 #include <QFileInfo>
28 #include <QDir>
29 #include <QProcess>
30 #include <QApplication>
31 #include <QDateTime>
32 #include <QTime>
33 #include <QWidget>
34 #include <QStyle>
35 #include <QDesktopWidget>
36 #include <QEventLoop>
37 #include <QStandardPaths>
38 #include <QSystemTrayIcon>
39 #include <QSet>
40 #include <QUrl>
41 #ifndef _MSC_VER
42 #include <unistd.h>
43 #include <utime.h>
44 #else
45 #include <sys/utime.h>
46 #endif
47 #include <sys/types.h>
48 #include <sys/stat.h>
49 #ifndef Q_OS_WIN
50 #include <grp.h>
51 #include <pwd.h>
52 #endif
53 #include <sys/types.h>
54 #if QT_QTDBUS_FOUND && !defined Q_OS_MAC && !defined Q_OS_WIN
55 #include <QDBusConnection>
56 #include <QDBusConnectionInterface>
57 #endif
58 
59 const QLatin1Char Utils::constDirSep('/');
60 const QLatin1String Utils::constDirSepStr("/");
61 const char * Utils::constDirSepCharStr="/";
62 
63 static const QLatin1String constHttp("http:");
64 static const QLatin1String constHttps("https:");
65 
fixPath(const QString & dir,bool ensureEndsInSlash)66 QString Utils::fixPath(const QString &dir, bool ensureEndsInSlash)
67 {
68     QString d(dir);
69 
70     if (!d.isEmpty() && !d.startsWith(constHttp) && !d.startsWith(constHttps)) {
71         #ifdef Q_OS_WIN
72         // Windows shares can be \\host\share (which gets converted to //host/share)
73         // so if th epath starts with // we need to keep this double slash.
74         bool startsWithDoubleSlash=d.length()>2 && d.startsWith(QLatin1String("//"));
75         #endif
76         d.replace(QLatin1String("//"), constDirSepStr);
77         #ifdef Q_OS_WIN
78         if (startsWithDoubleSlash) { // Re add first slash
79             d=QLatin1Char('/')+d;
80         }
81         #endif
82     }
83     d.replace(QLatin1String("/./"), constDirSepStr);
84     if (ensureEndsInSlash && !d.isEmpty() && !d.endsWith(constDirSep)) {
85         d+=constDirSep;
86     }
87     return d;
88 }
89 
90 #ifndef Q_OS_WIN
91 static const QLatin1String constTilda("~");
homeToTilda(const QString & s)92 QString Utils::homeToTilda(const QString &s)
93 {
94     QString hp=QDir::homePath();
95     if (s==hp) {
96         return constTilda;
97     }
98     if (s.startsWith(hp+constDirSepStr)) {
99         return constTilda+fixPath(s.mid(hp.length()), false);
100     }
101     return s;
102 }
103 
tildaToHome(const QString & s)104 QString Utils::tildaToHome(const QString &s)
105 {
106     if (s==constTilda) {
107         return fixPath(QDir::homePath());
108     }
109     if (s.startsWith(constTilda+constDirSep)) {
110         return fixPath(QDir::homePath()+constDirSepStr+s.mid(1), false);
111     }
112     return s;
113 }
114 #endif
115 
getDir(const QString & file,bool addSlash)116 QString Utils::getDir(const QString &file, bool addSlash)
117 {
118     bool isCueFile=file.contains("/cue:///") && file.contains("?pos=");
119     QString d(file);
120     int slashPos(d.lastIndexOf(constDirSep));
121 
122     if (slashPos!=-1) {
123         d.remove(slashPos+1, d.length());
124     }
125 
126     if (isCueFile) {
127         d.remove("cue:///");
128     }
129     if (!addSlash) {
130         if (d.endsWith("/")) {
131             return d.left(d.length()-1);
132         }
133         return d;
134     }
135     return fixPath(d);
136 }
137 
getFile(const QString & file)138 QString Utils::getFile(const QString &file)
139 {
140     QString f(file);
141     int slashPos=f.lastIndexOf(constDirSep);
142 
143     if (-1!=slashPos) {
144         f.remove(0, slashPos+1);
145     }
146 
147     return f;
148 }
149 
getExtension(const QString & file)150 QString Utils::getExtension(const QString &file)
151 {
152     QString f(file);
153     int dotPos=f.lastIndexOf('.');
154 
155     if (-1!=dotPos) {
156         return f.mid(dotPos);
157     }
158 
159     return QString();
160 }
161 
changeExtension(const QString & file,const QString & extension)162 QString Utils::changeExtension(const QString &file, const QString &extension)
163 {
164     if (extension.isEmpty()) {
165         return file;
166     }
167 
168     QString f(file);
169     int pos=f.lastIndexOf('.');
170     if (pos>1) {
171         f=f.left(pos+1);
172     }
173 
174     if (f.endsWith('.')) {
175         return f+(extension.startsWith('.') ? extension.mid(1) : extension);
176     }
177     return f+(extension.startsWith('.') ? extension : (QChar('.')+extension));
178 }
179 
isDirReadable(const QString & dir)180 bool Utils::isDirReadable(const QString &dir)
181 {
182     #ifdef Q_OS_WIN
183     if (dir.isEmpty()) {
184         return false;
185     } else {
186         QDir d(dir);
187         bool dirReadable=d.isReadable();
188         // Handle cases where dir is set to \\server\ (i.e. no shared folder is set in path)
189         if (!dirReadable && dir.startsWith(QLatin1String("//")) && d.isRoot() && (dir.length()-1)==dir.indexOf(Utils::constDirSep, 2)) {
190             dirReadable=true;
191         }
192         return dirReadable;
193     }
194     #else
195     return dir.isEmpty() ? false : QDir(dir).isReadable();
196     #endif
197 }
198 
strippedText(QString s)199 QString Utils::strippedText(QString s)
200 {
201     s.remove(QString::fromLatin1("..."));
202     int i = 0;
203     while (i < s.size()) {
204         ++i;
205         if (s.at(i - 1) != QLatin1Char('&')) {
206             continue;
207         }
208 
209         if (i < s.size() && s.at(i) == QLatin1Char('&')) {
210             ++i;
211         }
212         s.remove(i - 1, 1);
213     }
214     return s.trimmed();
215 }
216 
stripAcceleratorMarkers(QString label)217 QString Utils::stripAcceleratorMarkers(QString label)
218 {
219     int p = 0;
220     forever {
221         p = label.indexOf('&', p);
222         if(p < 0 || p + 1 >= label.length()) {
223             break;
224         }
225 
226         if(label.at(p + 1).isLetterOrNumber() || label.at(p + 1) == '&') {
227             label.remove(p, 1);
228         }
229 
230         ++p;
231     }
232     return label;
233 }
234 
hashParams(const QString & url)235 QMap<QString, QString> Utils::hashParams(const QString &url)
236 {
237     QMap<QString, QString> map;
238     int start=url.indexOf("#");
239     if (start>0) {
240         QStringList parts = url.mid(start+1).split("&");
241         for (const QString &p: parts) {
242             QStringList kv = p.split("=");
243             if (kv.length()>=2) {
244                 QString key = kv[0];
245                 kv.removeAt(0);
246                 map.insert(key, QUrl::fromPercentEncoding(kv.join("=").toLatin1()));
247             } else {
248                 map.insert(QString("-"), QUrl::fromPercentEncoding(p.toLatin1()));
249             }
250         }
251     }
252     return map;
253 }
254 
addHashParam(const QString & url,const QString & key,const QString & val)255 QString Utils::addHashParam(const QString &url, const QString &key, const QString &val)
256 {
257     if (val.isEmpty()) {
258         return url;
259     }
260 
261     return url+(url.contains('#') ? "&" : "#")+(key.isEmpty() ? "" : (key+"="))+QUrl::toPercentEncoding(val);
262 }
263 
removeHash(const QString & url)264 QString Utils::removeHash(const QString &url)
265 {
266     int hash=url.indexOf('#');
267     return hash<0 ? url : url.left(hash);
268 }
269 
convertPathForDisplay(const QString & path,bool isFolder)270 QString Utils::convertPathForDisplay(const QString &path, bool isFolder)
271 {
272     if (path.isEmpty() || path.startsWith(constHttp) || path.startsWith(constHttps)) {
273         return path;
274     }
275 
276     QString p(path);
277     if (p.endsWith(constDirSep)) {
278         p=p.left(p.length()-1);
279     }
280     /* TODO: Display ~/Music or /home/user/Music / /Users/user/Music ???
281     p=homeToTilda(QDir::toNativeSeparators(p));
282     */
283     return QDir::toNativeSeparators(isFolder && p.endsWith(constDirSep) ? p.left(p.length()-1) : p);
284 }
285 
convertPathFromDisplay(const QString & path,bool isFolder)286 QString Utils::convertPathFromDisplay(const QString &path, bool isFolder)
287 {
288     QString p=path.trimmed();
289     if (p.isEmpty()) {
290         return p;
291     }
292 
293     if (p.startsWith(constHttp) || p.startsWith(constHttps)) {
294         return fixPath(p);
295     }
296     return tildaToHome(fixPath(QDir::fromNativeSeparators(p), isFolder));
297 }
298 
299 #ifndef Q_OS_WIN
getGroupId(const char * groupName)300 gid_t Utils::getGroupId(const char *groupName)
301 {
302     static bool init=false;
303     static gid_t gid=0;
304 
305     if (init) {
306         return gid;
307     }
308 
309     init=true;
310 
311     // First of all see if current group is actually 'groupName'!!!
312     gid_t egid=getegid();
313     struct group *group=getgrgid(egid);
314 
315     if (group && 0==strcmp(group->gr_name, groupName)) {
316         gid=egid;
317         return gid;
318     }
319 
320     // Now see if user is a member of 'groupName'
321     struct passwd *pw=getpwuid(geteuid());
322 
323     if (!pw) {
324         return gid;
325     }
326 
327     group=getgrnam(groupName);
328 
329     if (group) {
330         for (int i=0; group->gr_mem[i]; ++i) {
331             if (0==strcmp(group->gr_mem[i], pw->pw_name)) {
332                 gid=group->gr_gid;
333                 return gid;
334             }
335         }
336     }
337     return gid;
338 }
339 
340 /*
341  * Set file permissions.
342  * If user is a memeber of "users" group, then set file as owned by and writeable by "users" group.
343  */
setFilePerms(const QString & file,const char * groupName)344 void Utils::setFilePerms(const QString &file, const char *groupName)
345 {
346     //
347     // Clear any umask before setting file perms
348     mode_t oldMask(umask(0000));
349     gid_t gid=getGroupId(groupName);
350     QByteArray fn=QFile::encodeName(file);
351     ::chmod(fn.constData(), 0==gid ? 0644 : 0664);
352     if (0!=gid) {
353         int rv=::chown(fn.constData(), geteuid(), gid);
354         Q_UNUSED(rv)
355     }
356     // Reset umask
357     ::umask(oldMask);
358 }
359 #else
setFilePerms(const QString & file,const char * groupName)360 void Utils::setFilePerms(const QString &file, const char *groupName)
361 {
362     Q_UNUSED(file)
363     Q_UNUSED(groupName)
364 }
365 #endif
366 
367 /*
368  * Create directory, and set its permissions.
369  * If user is a memeber of "audio" group, then set dir as owned by and writeable by "audio" group.
370  */
createWorldReadableDir(const QString & dir,const QString & base,const char * groupName)371 bool Utils::createWorldReadableDir(const QString &dir, const QString &base, const char *groupName)
372 {
373     #ifdef Q_OS_WIN
374     Q_UNUSED(base)
375     Q_UNUSED(groupName)
376     return makeDir(dir, 0775);
377     #else
378     //
379     // Clear any umask before dir is created
380     mode_t oldMask(umask(0000));
381     gid_t gid=base.isEmpty() ? 0 : getGroupId(groupName);
382     bool status(makeDir(dir, 0==gid ? 0755 : 0775));
383 
384     if (status && 0!=gid && dir.startsWith(base)) {
385         QStringList parts=dir.mid(base.length()).split(constDirSep);
386         QString d(base);
387 
388         for (const QString &p: parts) {
389             d+=constDirSep+p;
390             int rv=::chown(QFile::encodeName(d).constData(), geteuid(), gid);
391             Q_UNUSED(rv)
392         }
393     }
394     // Reset umask
395     ::umask(oldMask);
396     return status;
397     #endif
398 }
399 
400 // Copied from KDE... START
401 #include <QLocale>
402 #include <fcntl.h>
403 #include <sys/stat.h>
404 
405 // kde_file.h
406 #ifndef Q_OS_WIN
407 #if (defined _LFS64_LARGEFILE) && (defined _LARGEFILE64_SOURCE) && (!defined _GNU_SOURCE) && (!defined __sun)
408 #define KDE_stat                ::stat64
409 #define KDE_lstat               ::lstat64
410 #define KDE_struct_stat         struct stat64
411 #define KDE_mkdir               ::mkdir
412 #else
413 #define KDE_stat                ::stat
414 #define KDE_lstat               ::lstat
415 #define KDE_struct_stat         struct stat
416 #define KDE_mkdir               ::mkdir
417 #endif
418 #endif // Q_OS_WIN
419 
420 // kstandarddirs.h
makeDir(const QString & dir,int mode)421 bool Utils::makeDir(const QString &dir, int mode)
422 {
423     // we want an absolute path
424     if (QDir::isRelativePath(dir)) {
425         return false;
426     }
427 
428     #ifdef Q_OS_WIN
429     Q_UNUSED(mode)
430     return QDir().mkpath(dir);
431     #else
432     QString target = dir;
433     uint len = target.length();
434 
435     // append trailing slash if missing
436     if (dir.at(len - 1) != QLatin1Char('/')) {
437         target += QLatin1Char('/');
438     }
439 
440     QString base;
441     uint i = 1;
442 
443     while ( i < len ) {
444         KDE_struct_stat st;
445         int pos = target.indexOf(QLatin1Char('/'), i);
446         base += target.mid(i - 1, pos - i + 1);
447         QByteArray baseEncoded = QFile::encodeName(base);
448         // bail out if we encountered a problem
449         if (KDE_stat(baseEncoded, &st) != 0) {
450             // Directory does not exist....
451             // Or maybe a dangling symlink ?
452             if (KDE_lstat(baseEncoded, &st) == 0)
453                 (void)unlink(baseEncoded); // try removing
454 
455             if (KDE_mkdir(baseEncoded, static_cast<mode_t>(mode)) != 0) {
456                 baseEncoded.prepend( "trying to create local folder " );
457                 perror(baseEncoded.constData());
458                 return false; // Couldn't create it :-(
459             }
460         }
461         i = pos + 1;
462     }
463     return true;
464     #endif
465 }
466 
formatByteSize(double size)467 QString Utils::formatByteSize(double size)
468 {
469     static bool useSiUnites=false;
470     static QLocale locale;
471 
472     #ifndef Q_OS_WIN
473     static bool init=false;
474     if (!init) {
475         init=true;
476         const char *env=qgetenv("KDE_FULL_SESSION");
477         QString dm=env && 0==strcmp(env, "true") ? QLatin1String("KDE") : QString(qgetenv("XDG_CURRENT_DESKTOP"));
478         useSiUnites=!dm.isEmpty() && QLatin1String("KDE")!=dm;
479     }
480     #endif
481     int unit = 0;
482     double multiplier = useSiUnites ? 1000.0 : 1024.0;
483 
484     while (qAbs(size) >= multiplier && unit < 3) {
485         size /= multiplier;
486         unit++;
487     }
488 
489     if (useSiUnites) {
490         switch(unit) {
491         case 0: return QObject::tr("%1 B").arg(size);
492         case 1: return QObject::tr("%1 kB").arg(locale.toString(size, 'f', 1));
493         case 2: return QObject::tr("%1 MB").arg(locale.toString(size, 'f', 1));
494         default:
495         case 3: return QObject::tr("%1 GB").arg(locale.toString(size, 'f', 1));
496         }
497     } else {
498         switch(unit) {
499         case 0: return QObject::tr("%1 B").arg(size);
500         case 1: return QObject::tr("%1 KiB").arg(locale.toString(size, 'f', 1));
501         case 2: return QObject::tr("%1 MiB").arg(locale.toString(size, 'f', 1));
502         default:
503         case 3: return QObject::tr("%1 GiB").arg(locale.toString(size, 'f', 1));
504         }
505     }
506 }
507 
508 #if defined Q_OS_WIN
509 #define KPATH_SEPARATOR ';'
510 // #define KDIR_SEPARATOR '\\' /* faster than QDir::separator() */
511 #else
512 #define KPATH_SEPARATOR ':'
513 // #define KDIR_SEPARATOR '/' /* faster than QDir::separator() */
514 #endif
515 
equalizePath(QString & str)516 static inline QString equalizePath(QString &str)
517 {
518     #ifdef Q_OS_WIN
519     // filter pathes through QFileInfo to have always
520     // the same case for drive letters
521     QFileInfo f(str);
522     if (f.isAbsolute())
523         return f.absoluteFilePath();
524     else
525     #endif
526         return str;
527 }
528 
tokenize(QStringList & tokens,const QString & str,const QString & delim)529 static void tokenize(QStringList &tokens, const QString &str, const QString &delim)
530 {
531     const int len = str.length();
532     QString token;
533 
534     for(int index = 0; index < len; index++) {
535         if (delim.contains(str[index])) {
536             tokens.append(equalizePath(token));
537             token.clear();
538         } else {
539             token += str[index];
540         }
541     }
542     if (!token.isEmpty()) {
543         tokens.append(equalizePath(token));
544     }
545 }
546 
547 #ifdef Q_OS_WIN
executableExtensions()548 static QStringList executableExtensions()
549 {
550     QStringList ret = QString::fromLocal8Bit(qgetenv("PATHEXT")).split(QLatin1Char(';'));
551     if (!ret.contains(QLatin1String(".exe"), Qt::CaseInsensitive)) {
552         // If %PATHEXT% does not contain .exe, it is either empty, malformed, or distorted in ways that we cannot support, anyway.
553         ret.clear();
554         ret << QLatin1String(".exe")
555             << QLatin1String(".com")
556             << QLatin1String(".bat")
557             << QLatin1String(".cmd");
558     }
559     return ret;
560 }
561 #endif
562 
systemPaths(const QString & pstr)563 static QStringList systemPaths(const QString &pstr)
564 {
565     QStringList tokens;
566     QString p = pstr;
567 
568     if( p.isEmpty() ) {
569         p = QString::fromLocal8Bit( qgetenv( "PATH" ) );
570     }
571 
572     QString delimiters(QLatin1Char(KPATH_SEPARATOR));
573     delimiters += QLatin1Char('\b');
574     tokenize( tokens, p, delimiters );
575 
576     QStringList exePaths;
577 
578     // split path using : or \b as delimiters
579     for( int i = 0; i < tokens.count(); i++ ) {
580         exePaths << /*KShell::tildeExpand(*/ tokens[ i ] /*)*/; // TODO
581     }
582 
583     return exePaths;
584 }
585 
586 #ifdef Q_OS_MAC
getBundle(const QString & path)587 static QString getBundle(const QString &path)
588 {
589     //kDebug(180) << "getBundle(" << path << ", " << ignore << ") called";
590     QFileInfo info;
591     QString bundle = path;
592     bundle += QLatin1String(".app/Contents/MacOS/") + bundle.section(QLatin1Char('/'), -1);
593     info.setFile( bundle );
594     FILE *file;
595     if ((file = fopen(info.absoluteFilePath().toUtf8().constData(), "r"))) {
596         fclose(file);
597         struct stat _stat;
598         if ((stat(info.absoluteFilePath().toUtf8().constData(), &_stat)) < 0) {
599             return QString();
600         }
601         if ( _stat.st_mode & S_IXUSR ) {
602             if ( ((_stat.st_mode & S_IFMT) == S_IFREG) || ((_stat.st_mode & S_IFMT) == S_IFLNK) ) {
603                 //kDebug(180) << "getBundle(): returning " << bundle;
604                 return bundle;
605             }
606         }
607     }
608     return QString();
609 }
610 #endif
611 
checkExecutable(const QString & path)612 static QString checkExecutable( const QString& path )
613 {
614     #ifdef Q_OS_MAC
615     QString bundle = getBundle( path );
616     if ( !bundle.isEmpty() ) {
617         //kDebug(180) << "findExe(): returning " << bundle;
618         return bundle;
619     }
620     #endif
621     QFileInfo info( path );
622     QFileInfo orig = info;
623     #if defined(Q_OS_DARWIN) || defined(Q_OS_MAC)
624     FILE *file;
625     if ((file = fopen(orig.absoluteFilePath().toUtf8().constData(), "r"))) {
626         fclose(file);
627         struct stat _stat;
628         if ((stat(orig.absoluteFilePath().toUtf8().constData(), &_stat)) < 0) {
629             return QString();
630         }
631         if ( _stat.st_mode & S_IXUSR ) {
632             if ( ((_stat.st_mode & S_IFMT) == S_IFREG) || ((_stat.st_mode & S_IFMT) == S_IFLNK) ) {
633                 orig.makeAbsolute();
634                 return orig.filePath();
635             }
636         }
637     }
638     return QString();
639     #else
640     if( info.exists() && info.isSymLink() )
641         info = QFileInfo( info.canonicalFilePath() );
642     if( info.exists() && info.isExecutable() && info.isFile() ) {
643         // return absolute path, but without symlinks resolved in order to prevent
644         // problems with executables that work differently depending on name they are
645         // run as (for example gunzip)
646         orig.makeAbsolute();
647         return orig.filePath();
648     }
649     //kDebug(180) << "checkExecutable(): failed, returning empty string";
650     return QString();
651     #endif
652 }
653 
findExe(const QString & appname,const QString & pstr)654 QString Utils::findExe(const QString &appname, const QString &pstr)
655 {
656     #ifdef Q_OS_WIN
657     QStringList executable_extensions = executableExtensions();
658     if (!executable_extensions.contains(appname.section(QLatin1Char('.'), -1, -1, QString::SectionIncludeLeadingSep), Qt::CaseInsensitive)) {
659         QString found_exe;
660         for (const QString& extension: executable_extensions) {
661             found_exe = findExe(appname + extension, pstr);
662             if (!found_exe.isEmpty()) {
663                 return found_exe;
664             }
665         }
666         return QString();
667     }
668     #endif
669 
670     const QStringList exePaths = systemPaths( pstr );
671     for (QStringList::ConstIterator it = exePaths.begin(); it != exePaths.end(); ++it) {
672         QString p = (*it) + QLatin1Char('/');
673         p += appname;
674 
675         QString result = checkExecutable(p);
676         if (!result.isEmpty()) {
677             return result;
678         }
679     }
680 
681     return QString();
682 }
683 // Copied from KDE... END
684 
formatDuration(const quint32 totalseconds)685 QString Utils::formatDuration(const quint32 totalseconds)
686 {
687     //Get the days,hours,minutes and seconds out of the total seconds
688     quint32 days = totalseconds / 86400;
689     quint32 rest = totalseconds - (days * 86400);
690     quint32 hours = rest / 3600;
691     rest = rest - (hours * 3600);
692     quint32 minutes = rest / 60;
693     quint32 seconds = rest - (minutes * 60);
694 
695     //Convert hour,minutes and seconds to a QTime for easier parsing
696     QTime time(hours, minutes, seconds);
697 
698     return 0==days
699             ? time.toString("h:mm:ss")
700             : QString("%1:%2").arg(days).arg(time.toString("hh:mm:ss"));
701 }
702 
formatTime(const quint32 seconds,bool zeroIsUnknown)703 QString Utils::formatTime(const quint32 seconds, bool zeroIsUnknown)
704 {
705     if (0==seconds && zeroIsUnknown) {
706         return QObject::tr("Unknown");
707     }
708 
709     static const quint32 constHour=60*60;
710     if (seconds>constHour) {
711         return Utils::formatDuration(seconds);
712     }
713 
714     QString result(QString::number(floor(seconds / 60.0))+QChar(':'));
715     if (seconds % 60 < 10) {
716         result += "0";
717     }
718     return result+QString::number(seconds % 60);
719 }
720 
cleanPath(const QString & p)721 QString Utils::cleanPath(const QString &p)
722 {
723     QString path(p);
724     while (path.contains("//")) {
725         path.replace("//", constDirSepStr);
726     }
727     return fixPath(path);
728 }
729 
userDir(const QString & mainDir,const QString & sub,bool create)730 static QString userDir(const QString &mainDir, const QString &sub, bool create)
731 {
732     QString dir=mainDir;
733     if (!sub.isEmpty()) {
734         dir+=sub;
735     }
736     dir=Utils::cleanPath(dir);
737     QDir d(dir);
738     return d.exists() || (create && d.mkpath(dir)) ? dir : QString();
739 }
740 
dataDir(const QString & sub,bool create)741 QString Utils::dataDir(const QString &sub, bool create)
742 {
743     #if defined Q_OS_WIN || defined Q_OS_MAC
744 
745     return userDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)+constDirSep, sub, create);
746 
747     #else
748 
749     static QString location;
750     if (location.isEmpty()) {
751         location=QStandardPaths::writableLocation(QStandardPaths::DataLocation);
752         if (QCoreApplication::organizationName()==QCoreApplication::applicationName()) {
753             location=location.replace(QCoreApplication::organizationName()+Utils::constDirSep+QCoreApplication::applicationName(),
754                                       QCoreApplication::applicationName());
755         }
756     }
757     return userDir(location+constDirSep, sub, create);
758 
759     #endif
760 }
761 
cacheDir(const QString & sub,bool create)762 QString Utils::cacheDir(const QString &sub, bool create)
763 {
764     #if defined Q_OS_WIN || defined Q_OS_MAC
765 
766     return userDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)+constDirSep, sub, create);
767 
768     #else
769 
770     static QString location;
771     if (location.isEmpty()) {
772         location=QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
773         if (QCoreApplication::organizationName()==QCoreApplication::applicationName()) {
774             location=location.replace(QCoreApplication::organizationName()+Utils::constDirSep+QCoreApplication::applicationName(),
775                                       QCoreApplication::applicationName());
776         }
777     }
778     return userDir(location+constDirSep, sub, create);
779 
780     #endif
781 }
782 
systemDir(const QString & sub)783 QString Utils::systemDir(const QString &sub)
784 {
785     #if defined Q_OS_WIN
786     return fixPath(QCoreApplication::applicationDirPath()+constDirSep+(sub.isEmpty() ? QString() : (sub+constDirSep)));
787     #elif defined Q_OS_MAC
788     return fixPath(QCoreApplication::applicationDirPath()+QLatin1String("/../Resources/")+(sub.isEmpty() ? QString() : (sub+constDirSep)));
789     #else
790     return fixPath(QString(SHARE_INSTALL_PREFIX"/")+QCoreApplication::applicationName()+constDirSep+(sub.isEmpty() ? QString() : (sub+constDirSep)));
791     #endif
792 }
793 
helper(const QString & app)794 QString Utils::helper(const QString &app)
795 {
796     #if defined Q_OS_WIN
797     return fixPath(QCoreApplication::applicationDirPath())+app+QLatin1String(".exe");
798     #elif defined Q_OS_MAC
799     return fixPath(QCoreApplication::applicationDirPath())+app;
800     #else
801     // Check for helpers in same folder as main exe, so that can test dev versions without install.
802     QString local = fixPath(QCoreApplication::applicationDirPath())+app;
803     if (QFile::exists(local)) {
804         return local;
805     }
806     return fixPath(QString(INSTALL_PREFIX "/" LINUX_LIB_DIR "/")+QCoreApplication::applicationName()+constDirSep)+app;
807     #endif
808 }
809 
moveFile(const QString & from,const QString & to)810 bool Utils::moveFile(const QString &from, const QString &to)
811 {
812     return !from.isEmpty() && !to.isEmpty() && from!=to && QFile::exists(from) && !QFile::exists(to) && QFile::rename(from, to);
813 }
814 
moveDir(const QString & from,const QString & to)815 void Utils::moveDir(const QString &from, const QString &to)
816 {
817     if (from.isEmpty() || to.isEmpty() || from==to) {
818         return;
819     }
820 
821     QDir f(from);
822     if (!f.exists()) {
823         return;
824     }
825 
826     QDir t(to);
827     if (!t.exists()) {
828         return;
829     }
830 
831     QFileInfoList files=f.entryInfoList(QStringList() << "*", QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot);
832     for (const QFileInfo &file: files) {
833         if (file.isDir()) {
834             QString dest=to+file.fileName()+constDirSep;
835             if (!QDir(dest).exists()) {
836                 t.mkdir(file.fileName());
837             }
838             moveDir(from+file.fileName()+constDirSep, dest);
839         } else {
840             QFile::rename(from+file.fileName(), to+file.fileName());
841         }
842     }
843 
844     f.cdUp();
845     f.rmdir(from);
846 }
847 
clearOldCache(const QString & sub,int maxAge)848 void Utils::clearOldCache(const QString &sub, int maxAge)
849 {
850     if (sub.isEmpty()) {
851         return;
852     }
853 
854     QString d=cacheDir(sub, false);
855     if (d.isEmpty()) {
856         return;
857     }
858 
859     QDir dir(d);
860     if (dir.exists()) {
861         QFileInfoList files=dir.entryInfoList(QDir::Files|QDir::NoDotAndDotDot);
862         if (files.count()) {
863             QDateTime now=QDateTime::currentDateTime();
864             for (const QFileInfo &f: files) {
865                 if (f.lastModified().daysTo(now)>maxAge) {
866                     QFile::remove(f.absoluteFilePath());
867                 }
868             }
869         }
870     }
871 }
872 
touchFile(const QString & fileName)873 void Utils::touchFile(const QString &fileName)
874 {
875     ::utime(QFile::encodeName(fileName).constData(), nullptr);
876 }
877 
smallFontFactor(const QFont & f)878 double Utils::smallFontFactor(const QFont &f)
879 {
880     double sz=f.pointSizeF();
881     if (sz<=8.5) {
882         return 1.0;
883     }
884     if (sz<=9.0) {
885         return 0.9;
886     }
887     return 0.85;
888 }
889 
smallFont(QFont f)890 QFont Utils::smallFont(QFont f)
891 {
892     f.setPointSizeF(f.pointSizeF()*smallFontFactor(f));
893     return f;
894 }
895 
layoutSpacing(QWidget * w)896 int Utils::layoutSpacing(QWidget *w)
897 {
898     int spacing=(w ? w->style() : qApp->style())->layoutSpacing(QSizePolicy::DefaultType, QSizePolicy::DefaultType, Qt::Vertical);
899     if (spacing<0) {
900         spacing=scaleForDpi(4);
901     }
902     return spacing;
903 }
904 
screenDpiScale()905 double Utils::screenDpiScale()
906 {
907     static double scaleFactor=-1.0;
908     if (scaleFactor<0) {
909         QWidget *dw=QApplication::desktop();
910         if (!dw) {
911             return 1.0;
912         }
913         scaleFactor=dw->logicalDpiX()>120 ? qMin(qMax(dw->logicalDpiX()/96.0, 1.0), 4.0) : 1.0;
914     }
915     return scaleFactor;
916 }
917 
limitedHeight(QWidget * w)918 bool Utils::limitedHeight(QWidget *w)
919 {
920     static bool init=false;
921     static bool limited=false;
922     if (!init) {
923         limited=!qgetenv("CANTATA_NETBOOK").isEmpty();
924         if (!limited) {
925             QDesktopWidget *dw=QApplication::desktop();
926             if (dw) {
927                 limited=dw->availableGeometry(w).size().height()<=800;
928             }
929         }
930     }
931     return limited;
932 }
933 
resizeWindow(QWidget * w,bool preserveWidth,bool preserveHeight)934 void Utils::resizeWindow(QWidget *w, bool preserveWidth, bool preserveHeight)
935 {
936     QWidget *window=w ? w->window() : nullptr;
937     if (window) {
938         QSize was=window->size();
939         QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
940         window->setMinimumSize(QSize(0, 0));
941         window->adjustSize();
942         QSize now=window->size();
943         window->setMinimumSize(now);
944         if (preserveWidth && preserveHeight) {
945             window->resize(qMax(was.width(), now.width()), qMax(was.height(), now.height()));
946         } else if (preserveWidth) {
947             window->resize(qMax(was.width(), now.width()), now.height());
948         } else if (preserveHeight) {
949             window->resize(now.width(), qMax(was.height(), now.height()));
950         }
951     }
952 }
953 
currentDe()954 Utils::Desktop Utils::currentDe()
955 {
956     #if !defined Q_OS_WIN && !defined Q_OS_MAC
957     static int de=-1;
958     if (-1==de) {
959         de=Other;
960         QSet<QByteArray> desktop=qgetenv("XDG_CURRENT_DESKTOP").toLower().split(':').toSet();
961         if (desktop.contains("unity")) {
962             de=Unity;
963         } else if (desktop.contains("kde")) {
964             de=KDE;
965         } else if (desktop.contains("gnome") || desktop.contains("pantheon")) {
966             de=Gnome;
967         } else {
968             QByteArray kde=qgetenv("KDE_FULL_SESSION");
969             if ("true"==kde) {
970                 de=KDE;
971             }
972         }
973     }
974     return (Utils::Desktop)de;
975     #endif
976     return Other;
977 }
978 
useSystemTray()979 bool Utils::useSystemTray()
980 {
981     #if defined Q_OS_MAC
982     return false;
983     #elif defined Q_OS_WIN
984     return true;
985     #elif QT_QTDBUS_FOUND
986     return (Gnome==currentDe() || Unity==currentDe())
987             ? QDBusConnection::sessionBus().interface()->isServiceRegistered("org.kde.StatusNotifierWatcher")
988             : QSystemTrayIcon::isSystemTrayAvailable();
989     #else
990     return Gnome!=currentDe() && Unity!=currentDe() && QSystemTrayIcon::isSystemTrayAvailable();
991     #endif
992 }
993 
buildPath(const QRectF & r,double radius)994 QPainterPath Utils::buildPath(const QRectF &r, double radius)
995 {
996     QPainterPath path;
997     double diameter(radius*2);
998 
999     path.moveTo(r.x()+r.width(), r.y()+r.height()-radius);
1000     path.arcTo(r.x()+r.width()-diameter, r.y(), diameter, diameter, 0, 90);
1001     path.arcTo(r.x(), r.y(), diameter, diameter, 90, 90);
1002     path.arcTo(r.x(), r.y()+r.height()-diameter, diameter, diameter, 180, 90);
1003     path.arcTo(r.x()+r.width()-diameter, r.y()+r.height()-diameter, diameter, diameter, 270, 90);
1004     return path;
1005 }
1006 
clampColor(const QColor & col)1007 QColor Utils::clampColor(const QColor &col)
1008 {
1009     static const int constMin=64;
1010     static const int constMax=240;
1011 
1012     if (col.value()<constMin) {
1013         return QColor(constMin, constMin, constMin);
1014     } else if (col.value()>constMax) {
1015         return QColor(constMax, constMax, constMax);
1016     }
1017     return col;
1018 }
1019 
monoIconColor()1020 QColor Utils::monoIconColor()
1021 {
1022     return clampColor(QApplication::palette().color(QPalette::Active, QPalette::WindowText));
1023 }
1024 
1025 #ifdef Q_OS_WIN
1026 // This is down here, because windows.h includes ALL windows stuff - and we get conflicts with MessageBox :-(
1027 #include <windows.h>
1028 #endif
1029 
raiseWindow(QWidget * w)1030 void Utils::raiseWindow(QWidget *w)
1031 {
1032     if (!w) {
1033         return;
1034     }
1035 
1036     #ifdef Q_OS_WIN
1037     ::SetWindowPos(reinterpret_cast<HWND>(w->effectiveWinId()), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
1038     ::SetWindowPos(reinterpret_cast<HWND>(w->effectiveWinId()), HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
1039     #elif !defined Q_OS_WIN
1040     bool wasHidden=w->isHidden();
1041     #endif
1042 
1043     w->raise();
1044     w->showNormal();
1045     w->activateWindow();
1046     #ifdef Q_OS_MAC
1047     w->raise();
1048     #endif
1049     #if !defined Q_OS_WIN && !defined Q_OS_MAC
1050     // This section seems to be required for compiz, so that MPRIS.Raise actually shows the window, and not just highlight launcher.
1051     QString wmctrl=Utils::findExe(QLatin1String("wmctrl"));
1052     if (!wmctrl.isEmpty()) {
1053         if (wasHidden) {
1054             QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
1055         }
1056         QProcess::execute(wmctrl, QStringList() << QLatin1String("-i") << QLatin1String("-a") << QString::number(w->effectiveWinId()));
1057     }
1058     #endif
1059 }
1060 
1061