modernize: Reformat ALL the source... again!
[quassel.git] / src / common / quassel.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include "quassel.h"
22
23 #include <algorithm>
24 #include <iostream>
25
26 #include <QCoreApplication>
27 #include <QDateTime>
28 #include <QDir>
29 #include <QFileInfo>
30 #include <QHostAddress>
31 #include <QLibraryInfo>
32 #include <QMetaEnum>
33 #include <QSettings>
34 #include <QTranslator>
35 #include <QUuid>
36
37 #include "bufferinfo.h"
38 #include "identity.h"
39 #include "logger.h"
40 #include "logmessage.h"
41 #include "message.h"
42 #include "network.h"
43 #include "peer.h"
44 #include "protocol.h"
45 #include "syncableobject.h"
46 #include "types.h"
47 #include "version.h"
48
49 #ifndef Q_OS_WIN
50 #    include "posixsignalwatcher.h"
51 #else
52 #    include "windowssignalwatcher.h"
53 #endif
54
55 Quassel::Quassel()
56     : Singleton<Quassel>{this}
57     , _logger{new Logger{this}}
58 {
59 #ifdef EMBED_DATA
60     Q_INIT_RESOURCE(i18n);
61 #endif
62 }
63
64 void Quassel::init(RunMode runMode)
65 {
66     _runMode = runMode;
67
68     qsrand(QTime(0, 0, 0).secsTo(QTime::currentTime()));
69
70     setupSignalHandling();
71     setupEnvironment();
72     registerMetaTypes();
73
74     // Initial translation (may be overridden in UI settings)
75     loadTranslation(QLocale::system());
76
77     setupCliParser();
78
79     // Don't keep a debug log on the core
80     logger()->setup(runMode != RunMode::CoreOnly);
81
82     Network::setDefaultCodecForServer("UTF-8");
83     Network::setDefaultCodecForEncoding("UTF-8");
84     Network::setDefaultCodecForDecoding("ISO-8859-15");
85 }
86
87 Logger* Quassel::logger() const
88 {
89     return _logger;
90 }
91
92 void Quassel::registerQuitHandler(QuitHandler handler)
93 {
94     instance()->_quitHandlers.emplace_back(std::move(handler));
95 }
96
97 void Quassel::quit()
98 {
99     // Protect against multiple invocations (e.g. triggered by MainWin::closeEvent())
100     if (!_quitting) {
101         _quitting = true;
102         quInfo() << "Quitting...";
103         if (_quitHandlers.empty()) {
104             QCoreApplication::quit();
105         }
106         else {
107             // Note: We expect one of the registered handlers to call QCoreApplication::quit()
108             for (auto&& handler : _quitHandlers) {
109                 handler();
110             }
111         }
112     }
113 }
114
115 void Quassel::registerReloadHandler(ReloadHandler handler)
116 {
117     instance()->_reloadHandlers.emplace_back(std::move(handler));
118 }
119
120 bool Quassel::reloadConfig()
121 {
122     bool result{true};
123     for (auto&& handler : _reloadHandlers) {
124         result = result && handler();
125     }
126     return result;
127 }
128
129 //! Register our custom types with Qt's Meta Object System.
130 /**  This makes them available for QVariant and in signals/slots, among other things.
131  *
132  */
133 void Quassel::registerMetaTypes()
134 {
135     // Complex types
136     qRegisterMetaType<Message>("Message");
137     qRegisterMetaType<BufferInfo>("BufferInfo");
138     qRegisterMetaType<NetworkInfo>("NetworkInfo");
139     qRegisterMetaType<Network::Server>("Network::Server");
140     qRegisterMetaType<Identity>("Identity");
141
142     qRegisterMetaTypeStreamOperators<Message>("Message");
143     qRegisterMetaTypeStreamOperators<BufferInfo>("BufferInfo");
144     qRegisterMetaTypeStreamOperators<NetworkInfo>("NetworkInfo");
145     qRegisterMetaTypeStreamOperators<Network::Server>("Network::Server");
146     qRegisterMetaTypeStreamOperators<Identity>("Identity");
147
148     qRegisterMetaType<IdentityId>("IdentityId");
149     qRegisterMetaType<BufferId>("BufferId");
150     qRegisterMetaType<NetworkId>("NetworkId");
151     qRegisterMetaType<UserId>("UserId");
152     qRegisterMetaType<AccountId>("AccountId");
153     qRegisterMetaType<MsgId>("MsgId");
154
155     qRegisterMetaType<QHostAddress>("QHostAddress");
156     qRegisterMetaTypeStreamOperators<QHostAddress>("QHostAddress");
157     qRegisterMetaType<QUuid>("QUuid");
158     qRegisterMetaTypeStreamOperators<QUuid>("QUuid");
159
160     qRegisterMetaTypeStreamOperators<IdentityId>("IdentityId");
161     qRegisterMetaTypeStreamOperators<BufferId>("BufferId");
162     qRegisterMetaTypeStreamOperators<NetworkId>("NetworkId");
163     qRegisterMetaTypeStreamOperators<UserId>("UserId");
164     qRegisterMetaTypeStreamOperators<AccountId>("AccountId");
165     qRegisterMetaTypeStreamOperators<MsgId>("MsgId");
166
167     qRegisterMetaType<Protocol::SessionState>("Protocol::SessionState");
168     qRegisterMetaType<PeerPtr>("PeerPtr");
169     qRegisterMetaTypeStreamOperators<PeerPtr>("PeerPtr");
170
171     // Versions of Qt prior to 4.7 didn't define QVariant as a meta type
172     if (!QMetaType::type("QVariant")) {
173         qRegisterMetaType<QVariant>("QVariant");
174         qRegisterMetaTypeStreamOperators<QVariant>("QVariant");
175     }
176 }
177
178 void Quassel::setupEnvironment()
179 {
180     // On modern Linux systems, XDG_DATA_DIRS contains a list of directories containing application data. This
181     // is, for example, used by Qt for finding icons and other things. In case Quassel is installed in a non-standard
182     // prefix (or run from the build directory), it makes sense to add this to XDG_DATA_DIRS so we don't have to
183     // hack extra search paths into various places.
184 #ifdef Q_OS_UNIX
185     QString xdgDataVar = QFile::decodeName(qgetenv("XDG_DATA_DIRS"));
186     if (xdgDataVar.isEmpty())
187         xdgDataVar = QLatin1String("/usr/local/share:/usr/share");  // sane defaults
188
189     QStringList xdgDirs = xdgDataVar.split(QLatin1Char(':'), QString::SkipEmptyParts);
190
191     // Add our install prefix (if we're not in a bindir, this just adds the current workdir)
192     QString appDir = QCoreApplication::applicationDirPath();
193     int binpos = appDir.lastIndexOf("/bin");
194     if (binpos >= 0) {
195         appDir.replace(binpos, 4, "/share");
196         xdgDirs.append(appDir);
197         // Also append apps/quassel, this is only for QIconLoader to find icons there
198         xdgDirs.append(appDir + "/apps/quassel");
199     }
200     else
201         xdgDirs.append(appDir);  // build directory is always the last fallback
202
203     xdgDirs.removeDuplicates();
204
205     qputenv("XDG_DATA_DIRS", QFile::encodeName(xdgDirs.join(":")));
206 #endif
207 }
208
209 void Quassel::setupBuildInfo()
210 {
211     BuildInfo buildInfo;
212     buildInfo.applicationName = "quassel";
213     buildInfo.coreApplicationName = "quasselcore";
214     buildInfo.clientApplicationName = "quasselclient";
215     buildInfo.organizationName = "Quassel Project";
216     buildInfo.organizationDomain = "quassel-irc.org";
217
218     buildInfo.protocolVersion = 10;  // FIXME: deprecated, will be removed
219
220     buildInfo.baseVersion = QUASSEL_VERSION_STRING;
221     buildInfo.generatedVersion = GIT_DESCRIBE;
222
223     // Check if we got a commit hash
224     if (!QString(GIT_HEAD).isEmpty()) {
225         buildInfo.commitHash = GIT_HEAD;
226         // Set to Unix epoch, wrapped as a string for backwards-compatibility
227         buildInfo.commitDate = QString::number(GIT_COMMIT_DATE);
228     }
229     else if (!QString(DIST_HASH).contains("Format")) {
230         buildInfo.commitHash = DIST_HASH;
231         // Leave as Unix epoch if set as Unix epoch, but don't force this for
232         // backwards-compatibility with existing packaging/release tools that might set strings.
233         buildInfo.commitDate = QString(DIST_DATE);
234     }
235
236     // create a nice version string
237     if (buildInfo.generatedVersion.isEmpty()) {
238         if (!buildInfo.commitHash.isEmpty()) {
239             // dist version
240             buildInfo.plainVersionString = QString{"v%1 (dist-%2)"}.arg(buildInfo.baseVersion).arg(buildInfo.commitHash.left(7));
241             buildInfo.fancyVersionString = QString{"v%1 (dist-<a href=\"https://github.com/quassel/quassel/commit/%3\">%2</a>)"}
242                                                .arg(buildInfo.baseVersion)
243                                                .arg(buildInfo.commitHash.left(7))
244                                                .arg(buildInfo.commitHash);
245         }
246         else {
247             // we only have a base version :(
248             buildInfo.plainVersionString = QString{"v%1 (unknown revision)"}.arg(buildInfo.baseVersion);
249         }
250     }
251     else {
252         // analyze what we got from git-describe
253         static const QRegExp rx{"(.*)-(\\d+)-g([0-9a-f]+)(-dirty)?$"};
254         if (rx.exactMatch(buildInfo.generatedVersion)) {
255             QString distance = rx.cap(2) == "0" ? QString{} : QString{"%1+%2 "}.arg(rx.cap(1), rx.cap(2));
256             buildInfo.plainVersionString = QString{"v%1 (%2git-%3%4)"}.arg(buildInfo.baseVersion, distance, rx.cap(3), rx.cap(4));
257             if (!buildInfo.commitHash.isEmpty()) {
258                 buildInfo.fancyVersionString = QString{"v%1 (%2git-<a href=\"https://github.com/quassel/quassel/commit/%5\">%3</a>%4)"}
259                                                    .arg(buildInfo.baseVersion, distance, rx.cap(3), rx.cap(4), buildInfo.commitHash);
260             }
261         }
262         else {
263             buildInfo.plainVersionString = QString{"v%1 (invalid revision)"}.arg(buildInfo.baseVersion);
264         }
265     }
266     if (buildInfo.fancyVersionString.isEmpty()) {
267         buildInfo.fancyVersionString = buildInfo.plainVersionString;
268     }
269
270     instance()->_buildInfo = std::move(buildInfo);
271 }
272
273 const Quassel::BuildInfo& Quassel::buildInfo()
274 {
275     return instance()->_buildInfo;
276 }
277
278 void Quassel::setupSignalHandling()
279 {
280 #ifndef Q_OS_WIN
281     _signalWatcher = new PosixSignalWatcher(this);
282 #else
283     _signalWatcher = new WindowsSignalWatcher(this);
284 #endif
285     connect(_signalWatcher, &AbstractSignalWatcher::handleSignal, this, &Quassel::handleSignal);
286 }
287
288 void Quassel::handleSignal(AbstractSignalWatcher::Action action)
289 {
290     switch (action) {
291     case AbstractSignalWatcher::Action::Reload:
292         // Most applications use this as the 'configuration reload' command, e.g. nginx uses it for graceful reloading of processes.
293         if (!_reloadHandlers.empty()) {
294             quInfo() << "Reloading configuration";
295             if (reloadConfig()) {
296                 quInfo() << "Successfully reloaded configuration";
297             }
298         }
299         break;
300     case AbstractSignalWatcher::Action::Terminate:
301         if (!_quitting) {
302             quit();
303         }
304         else {
305             quInfo() << "Already shutting down, ignoring signal";
306         }
307         break;
308     case AbstractSignalWatcher::Action::HandleCrash:
309         logBacktrace(instance()->coreDumpFileName());
310         exit(EXIT_FAILURE);
311     }
312 }
313
314 Quassel::RunMode Quassel::runMode()
315 {
316     return instance()->_runMode;
317 }
318
319 void Quassel::setupCliParser()
320 {
321     QList<QCommandLineOption> options;
322
323     // General options
324     /// @todo Bring back --datadir to specify the database location independent of config
325     if (runMode() == RunMode::ClientOnly) {
326         options += {{"c", "configdir"}, tr("Specify the directory holding the client configuration."), tr("path")};
327     }
328     else {
329         options += {{"c", "configdir"},
330                     tr("Specify the directory holding configuration files, the SQlite database and the SSL certificate."),
331                     tr("path")};
332     }
333
334     // Client options
335     if (runMode() != RunMode::CoreOnly) {
336         options += {
337             {"icontheme", tr("Override the system icon theme ('breeze' is recommended)."), tr("theme")},
338             {"qss", tr("Load a custom application stylesheet."), tr("file.qss")},
339             {"hidewindow", tr("Start the client minimized to the system tray.")},
340         };
341     }
342
343     // Core options
344     if (runMode() != RunMode::ClientOnly) {
345         options += {
346             {"listen", tr("The address(es) quasselcore will listen on."), tr("<address>[,<address>[,...]]"), "::,0.0.0.0"},
347             {{"p", "port"}, tr("The port quasselcore will listen at."), tr("port"), "4242"},
348             {{"n", "norestore"}, tr("Don't restore last core's state.")},
349             {"config-from-environment", tr("Load configuration from environment variables.")},
350             {"select-backend", tr("Switch storage backend (migrating data if possible)."), tr("backendidentifier")},
351             {"select-authenticator", tr("Select authentication backend."), tr("authidentifier")},
352             {"add-user", tr("Starts an interactive session to add a new core user.")},
353             {"change-userpass",
354              tr("Starts an interactive session to change the password of the user identified by <username>."),
355              tr("username")},
356             {"strict-ident", tr("Use users' quasselcore username as ident reply. Ignores each user's configured ident setting.")},
357             {"ident-daemon", tr("Enable internal ident daemon.")},
358             {"ident-port",
359              tr("The port quasselcore will listen at for ident requests. Only meaningful with --ident-daemon."),
360              tr("port"),
361              "10113"},
362             {"oidentd", tr("Enable oidentd integration. In most cases you should also enable --strict-ident.")},
363             {"oidentd-conffile", tr("Set path to oidentd configuration file."), tr("file")},
364 #ifdef HAVE_SSL
365             {"require-ssl", tr("Require SSL for remote (non-loopback) client connections.")},
366             {"ssl-cert", tr("Specify the path to the SSL certificate."), tr("path"), "configdir/quasselCert.pem"},
367             {"ssl-key", tr("Specify the path to the SSL key."), tr("path"), "ssl-cert-path"},
368 #endif
369         };
370     }
371
372     // Logging options
373     options += {
374         {{"L", "loglevel"}, tr("Supports one of Debug|Info|Warning|Error; default is Info."), tr("level"), "Info"},
375         {{"l", "logfile"}, tr("Log to a file."), "path"},
376 #ifdef HAVE_SYSLOG
377         {"syslog", tr("Log to syslog.")},
378 #endif
379     };
380
381     // Debug options
382     options += {{"d", "debug"}, tr("Enable debug output.")};
383     if (runMode() != RunMode::CoreOnly) {
384         options += {
385             {"debugbufferswitches", tr("Enables debugging for bufferswitches.")},
386             {"debugmodel", tr("Enables debugging for models.")},
387         };
388     }
389     if (runMode() != RunMode::ClientOnly) {
390         options += {
391             {"debug-irc", tr("Enable logging of all raw IRC messages to debug log, including passwords!  In most cases you should also set --loglevel Debug")},
392             {"debug-irc-id", tr("Limit raw IRC logging to this network ID.  Implies --debug-irc"), tr("database network ID"), "-1"},
393         };
394     }
395
396     _cliParser.addOptions(options);
397     _cliParser.addHelpOption();
398     _cliParser.addVersionOption();
399     _cliParser.setApplicationDescription(tr("Quassel IRC is a modern, distributed IRC client."));
400
401     // This will call ::exit() for --help, --version and in case of errors
402     _cliParser.process(*QCoreApplication::instance());
403 }
404
405 QString Quassel::optionValue(const QString& key)
406 {
407     return instance()->_cliParser.value(key);
408 }
409
410 bool Quassel::isOptionSet(const QString& key)
411 {
412     return instance()->_cliParser.isSet(key);
413 }
414
415 const QString& Quassel::coreDumpFileName()
416 {
417     if (_coreDumpFileName.isEmpty()) {
418         QDir configDir(configDirPath());
419         _coreDumpFileName = configDir.absoluteFilePath(
420             QString("Quassel-Crash-%1.log").arg(QDateTime::currentDateTime().toString("yyyyMMdd-hhmm")));
421         QFile dumpFile(_coreDumpFileName);
422         dumpFile.open(QIODevice::Append);
423         QTextStream dumpStream(&dumpFile);
424         dumpStream << "Quassel IRC: " << _buildInfo.baseVersion << ' ' << _buildInfo.commitHash << '\n';
425         qDebug() << "Quassel IRC: " << _buildInfo.baseVersion << ' ' << _buildInfo.commitHash;
426         dumpStream.flush();
427         dumpFile.close();
428     }
429     return _coreDumpFileName;
430 }
431
432 QString Quassel::configDirPath()
433 {
434     if (!instance()->_configDirPath.isEmpty())
435         return instance()->_configDirPath;
436
437     QString path;
438     if (isOptionSet("configdir")) {
439         path = Quassel::optionValue("configdir");
440     }
441     else {
442 #ifdef Q_OS_MAC
443         // On Mac, the path is always the same
444         path = QDir::homePath() + "/Library/Application Support/Quassel/";
445 #else
446         // We abuse QSettings to find us a sensible path on the other platforms
447 #    ifdef Q_OS_WIN
448         // don't use the registry
449         QSettings::Format format = QSettings::IniFormat;
450 #    else
451         QSettings::Format format = QSettings::NativeFormat;
452 #    endif
453         QSettings s(format, QSettings::UserScope, QCoreApplication::organizationDomain(), buildInfo().applicationName);
454         QFileInfo fileInfo(s.fileName());
455         path = fileInfo.dir().absolutePath();
456 #endif /* Q_OS_MAC */
457     }
458
459     path = QFileInfo{path}.absoluteFilePath();
460
461     if (!path.endsWith(QDir::separator()) && !path.endsWith('/'))
462         path += QDir::separator();
463
464     QDir qDir{path};
465     if (!qDir.exists(path)) {
466         if (!qDir.mkpath(path)) {
467             qCritical() << "Unable to create Quassel config directory:" << qPrintable(qDir.absolutePath());
468             return {};
469         }
470     }
471
472     instance()->_configDirPath = path;
473     return path;
474 }
475
476 QStringList Quassel::dataDirPaths()
477 {
478     if (!instance()->_dataDirPaths.isEmpty())
479         return instance()->_dataDirPaths;
480
481     // TODO: Migrate to QStandardPaths (will require moving of the sqlite database,
482     //       or a fallback for it being in the config dir)
483
484     QStringList dataDirNames;
485 #ifdef Q_OS_WIN
486     dataDirNames << qgetenv("APPDATA") + QCoreApplication::organizationDomain() + "/share/apps/quassel/"
487                  << qgetenv("APPDATA") + QCoreApplication::organizationDomain() << QCoreApplication::applicationDirPath();
488 #elif defined Q_OS_MAC
489     dataDirNames << QDir::homePath() + "/Library/Application Support/Quassel/" << QCoreApplication::applicationDirPath();
490 #else
491     // Linux et al
492
493     // XDG_DATA_HOME is the location for users to override system-installed files, usually in .local/share
494     // This should thus come first.
495     QString xdgDataHome = QFile::decodeName(qgetenv("XDG_DATA_HOME"));
496     if (xdgDataHome.isEmpty())
497         xdgDataHome = QDir::homePath() + QLatin1String("/.local/share");
498     dataDirNames << xdgDataHome;
499
500     // Now whatever is configured through XDG_DATA_DIRS
501     QString xdgDataDirs = QFile::decodeName(qgetenv("XDG_DATA_DIRS"));
502     if (xdgDataDirs.isEmpty())
503         dataDirNames << "/usr/local/share"
504                      << "/usr/share";
505     else
506         dataDirNames << xdgDataDirs.split(':', QString::SkipEmptyParts);
507
508     // Just in case, also check our install prefix
509     dataDirNames << QCoreApplication::applicationDirPath() + "/../share";
510
511     // Normalize and append our application name
512     for (int i = 0; i < dataDirNames.count(); i++)
513         dataDirNames[i] = QDir::cleanPath(dataDirNames.at(i)) + "/quassel/";
514
515 #endif
516
517     // Add resource path and workdir just in case.
518     // Workdir should have precedence
519     dataDirNames.prepend(QCoreApplication::applicationDirPath() + "/data/");
520     dataDirNames.append(":/data/");
521
522     // Append trailing '/' and check for existence
523     auto iter = dataDirNames.begin();
524     while (iter != dataDirNames.end()) {
525         if (!iter->endsWith(QDir::separator()) && !iter->endsWith('/'))
526             iter->append(QDir::separator());
527         if (!QFile::exists(*iter))
528             iter = dataDirNames.erase(iter);
529         else
530             ++iter;
531     }
532
533     dataDirNames.removeDuplicates();
534
535     instance()->_dataDirPaths = dataDirNames;
536     return dataDirNames;
537 }
538
539 QString Quassel::findDataFilePath(const QString& fileName)
540 {
541     QStringList dataDirs = dataDirPaths();
542     foreach (QString dataDir, dataDirs) {
543         QString path = dataDir + fileName;
544         if (QFile::exists(path))
545             return path;
546     }
547     return QString();
548 }
549
550 QStringList Quassel::scriptDirPaths()
551 {
552     QStringList res(configDirPath() + "scripts/");
553     foreach (QString path, dataDirPaths())
554         res << path + "scripts/";
555     return res;
556 }
557
558 QString Quassel::translationDirPath()
559 {
560     if (instance()->_translationDirPath.isEmpty()) {
561         // We support only one translation dir; fallback mechanisms wouldn't work else.
562         // This means that if we have a $data/translations dir, the internal :/i18n resource won't be considered.
563         foreach (const QString& dir, dataDirPaths()) {
564             if (QFile::exists(dir + "translations/")) {
565                 instance()->_translationDirPath = dir + "translations/";
566                 break;
567             }
568         }
569         if (instance()->_translationDirPath.isEmpty())
570             instance()->_translationDirPath = ":/i18n/";
571     }
572     return instance()->_translationDirPath;
573 }
574
575 void Quassel::loadTranslation(const QLocale& locale)
576 {
577     auto* qtTranslator = QCoreApplication::instance()->findChild<QTranslator*>("QtTr");
578     auto* quasselTranslator = QCoreApplication::instance()->findChild<QTranslator*>("QuasselTr");
579
580     if (qtTranslator)
581         qApp->removeTranslator(qtTranslator);
582     if (quasselTranslator)
583         qApp->removeTranslator(quasselTranslator);
584
585     // We use QLocale::C to indicate that we don't want a translation
586     if (locale.language() == QLocale::C)
587         return;
588
589     qtTranslator = new QTranslator(qApp);
590     qtTranslator->setObjectName("QtTr");
591
592     quasselTranslator = new QTranslator(qApp);
593     quasselTranslator->setObjectName("QuasselTr");
594
595 #ifndef Q_OS_MAC
596     bool success = qtTranslator->load(locale, QString("qt_"), translationDirPath());
597     if (!success)
598         qtTranslator->load(locale, QString("qt_"), QLibraryInfo::location(QLibraryInfo::TranslationsPath));
599     quasselTranslator->load(locale, QString(""), translationDirPath());
600 #else
601     bool success = qtTranslator->load(QString("qt_%1").arg(locale.name()), translationDirPath());
602     if (!success)
603         qtTranslator->load(QString("qt_%1").arg(locale.name()), QLibraryInfo::location(QLibraryInfo::TranslationsPath));
604     quasselTranslator->load(QString("%1").arg(locale.name()), translationDirPath());
605 #endif
606
607     qApp->installTranslator(quasselTranslator);
608     qApp->installTranslator(qtTranslator);
609 }
610
611 // ---- Quassel::Features ---------------------------------------------------------------------------------------------
612
613 Quassel::Features::Features()
614 {
615     QStringList features;
616
617     // TODO Qt5: Use QMetaEnum::fromType()
618     auto featureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("Feature"));
619     _features.resize(featureEnum.keyCount(), true);  // enable all known features to true
620 }
621
622 Quassel::Features::Features(const QStringList& features, LegacyFeatures legacyFeatures)
623 {
624     // TODO Qt5: Use QMetaEnum::fromType()
625     auto featureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("Feature"));
626     _features.resize(featureEnum.keyCount(), false);
627
628     for (auto&& feature : features) {
629         int i = featureEnum.keyToValue(qPrintable(feature));
630         if (i >= 0) {
631             _features[i] = true;
632         }
633         else {
634             _unknownFeatures << feature;
635         }
636     }
637
638     if (legacyFeatures) {
639         // TODO Qt5: Use QMetaEnum::fromType()
640         auto legacyFeatureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("LegacyFeature"));
641         for (quint32 mask = 0x0001; mask <= 0x8000; mask <<= 1) {
642             if (static_cast<quint32>(legacyFeatures) & mask) {
643                 int i = featureEnum.keyToValue(legacyFeatureEnum.valueToKey(mask));
644                 if (i >= 0) {
645                     _features[i] = true;
646                 }
647             }
648         }
649     }
650 }
651
652 bool Quassel::Features::isEnabled(Feature feature) const
653 {
654     auto i = static_cast<size_t>(feature);
655     return i < _features.size() ? _features[i] : false;
656 }
657
658 QStringList Quassel::Features::toStringList(bool enabled) const
659 {
660     // Check if any feature is enabled
661     if (!enabled && std::all_of(_features.cbegin(), _features.cend(), [](bool feature) { return !feature; })) {
662         return QStringList{} << "NoFeatures";
663     }
664
665     QStringList result;
666
667     // TODO Qt5: Use QMetaEnum::fromType()
668     auto featureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("Feature"));
669     for (quint32 i = 0; i < _features.size(); ++i) {
670         if (_features[i] == enabled) {
671             result << featureEnum.key(i);
672         }
673     }
674     return result;
675 }
676
677 Quassel::LegacyFeatures Quassel::Features::toLegacyFeatures() const
678 {
679     // TODO Qt5: Use LegacyFeatures (flag operators for enum classes not supported in Qt4)
680     quint32 result{0};
681     // TODO Qt5: Use QMetaEnum::fromType()
682     auto featureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("Feature"));
683     auto legacyFeatureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("LegacyFeature"));
684
685     for (quint32 i = 0; i < _features.size(); ++i) {
686         if (_features[i]) {
687             int v = legacyFeatureEnum.keyToValue(featureEnum.key(i));
688             if (v >= 0) {
689                 result |= v;
690             }
691         }
692     }
693     return static_cast<LegacyFeatures>(result);
694 }
695
696 QStringList Quassel::Features::unknownFeatures() const
697 {
698     return _unknownFeatures;
699 }