d335d0783a768edb2d9f7f4c23636073e53cb296
[quassel.git] / src / qtui / qtui.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 "qtui.h"
22
23 #include <QApplication>
24 #include <QFile>
25 #include <QFileInfo>
26 #include <QIcon>
27 #include <QStringList>
28
29 #include "abstractnotificationbackend.h"
30 #include "buffermodel.h"
31 #include "chatlinemodel.h"
32 #include "contextmenuactionprovider.h"
33 #include "icon.h"
34 #include "mainwin.h"
35 #include "qtuimessageprocessor.h"
36 #include "qtuisettings.h"
37 #include "qtuistyle.h"
38 #include "systemtray.h"
39 #include "toolbaractionprovider.h"
40 #include "types.h"
41 #include "util.h"
42
43 MainWin *QtUi::_mainWin = nullptr;
44 QList<AbstractNotificationBackend *> QtUi::_notificationBackends;
45 QList<AbstractNotificationBackend::Notification> QtUi::_notifications;
46
47 namespace {
48
49 QtUi *_instance{nullptr};
50
51 }
52
53
54 QtUi *QtUi::instance()
55 {
56     if (!_instance) {
57         _instance = new QtUi();
58     }
59     return _instance;
60 }
61
62
63 QtUi::QtUi()
64     : GraphicalUi()
65     , _systemIconTheme{QIcon::themeName()}
66 {
67     if (Quassel::isOptionSet("icontheme")) {
68         _systemIconTheme = Quassel::optionValue("icontheme");
69         QIcon::setThemeName(_systemIconTheme);
70     }
71
72     QtUiSettings uiSettings;
73     Quassel::loadTranslation(uiSettings.value("Locale", QLocale::system()).value<QLocale>());
74
75     setupIconTheme();
76
77     QApplication::setWindowIcon(icon::get("quassel"));
78
79     setContextMenuActionProvider(new ContextMenuActionProvider(this));
80     setToolBarActionProvider(new ToolBarActionProvider(this));
81
82     setUiStyle(new QtUiStyle(this));
83     _mainWin = new MainWin();
84
85     setMainWidget(_mainWin);
86
87     connect(_mainWin, SIGNAL(connectToCore(const QVariantMap &)), this, SIGNAL(connectToCore(const QVariantMap &)));
88     connect(_mainWin, SIGNAL(disconnectFromCore()), this, SIGNAL(disconnectFromCore()));
89     connect(Client::instance(), SIGNAL(bufferMarkedAsRead(BufferId)), SLOT(closeNotifications(BufferId)));
90 }
91
92
93 QtUi::~QtUi()
94 {
95     unregisterAllNotificationBackends();
96     delete _mainWin;
97     _mainWin = nullptr;
98     _instance = nullptr;
99 }
100
101
102 void QtUi::init()
103 {
104     _mainWin->init();
105     QtUiSettings uiSettings;
106     uiSettings.initAndNotify("UseSystemTrayIcon", this, SLOT(useSystemTrayChanged(QVariant)), true);
107
108     GraphicalUi::init(); // needs to be called after the mainWin is initialized
109 }
110
111
112 MessageModel *QtUi::createMessageModel(QObject *parent)
113 {
114     return new ChatLineModel(parent);
115 }
116
117
118 AbstractMessageProcessor *QtUi::createMessageProcessor(QObject *parent)
119 {
120     return new QtUiMessageProcessor(parent);
121 }
122
123
124 void QtUi::connectedToCore()
125 {
126     _mainWin->connectedToCore();
127 }
128
129
130 void QtUi::disconnectedFromCore()
131 {
132     _mainWin->disconnectedFromCore();
133     GraphicalUi::disconnectedFromCore();
134 }
135
136
137 void QtUi::useSystemTrayChanged(const QVariant &v)
138 {
139     _useSystemTray = v.toBool();
140     SystemTray *tray = mainWindow()->systemTray();
141     if (_useSystemTray) {
142         if (tray->isSystemTrayAvailable())
143             tray->setVisible(true);
144     }
145     else {
146         if (tray->isSystemTrayAvailable() && mainWindow()->isVisible())
147             tray->setVisible(false);
148     }
149 }
150
151
152 bool QtUi::haveSystemTray()
153 {
154     return mainWindow()->systemTray()->isSystemTrayAvailable() && instance()->_useSystemTray;
155 }
156
157
158 bool QtUi::isHidingMainWidgetAllowed() const
159 {
160     return haveSystemTray();
161 }
162
163
164 void QtUi::minimizeRestore(bool show)
165 {
166     SystemTray *tray = mainWindow()->systemTray();
167     if (show) {
168         if (tray && !_useSystemTray)
169             tray->setVisible(false);
170     }
171     else {
172         if (tray && _useSystemTray)
173             tray->setVisible(true);
174     }
175     GraphicalUi::minimizeRestore(show);
176 }
177
178
179 void QtUi::registerNotificationBackend(AbstractNotificationBackend *backend)
180 {
181     if (!_notificationBackends.contains(backend)) {
182         _notificationBackends.append(backend);
183         instance()->connect(backend, SIGNAL(activated(uint)), SLOT(notificationActivated(uint)));
184     }
185 }
186
187
188 void QtUi::unregisterNotificationBackend(AbstractNotificationBackend *backend)
189 {
190     _notificationBackends.removeAll(backend);
191 }
192
193
194 void QtUi::unregisterAllNotificationBackends()
195 {
196     _notificationBackends.clear();
197 }
198
199
200 const QList<AbstractNotificationBackend *> &QtUi::notificationBackends()
201 {
202     return _notificationBackends;
203 }
204
205
206 uint QtUi::invokeNotification(BufferId bufId, AbstractNotificationBackend::NotificationType type, const QString &sender, const QString &text)
207 {
208     static int notificationId = 0;
209
210     AbstractNotificationBackend::Notification notification(++notificationId, bufId, type, sender, text);
211     _notifications.append(notification);
212     foreach(AbstractNotificationBackend *backend, _notificationBackends)
213     backend->notify(notification);
214     return notificationId;
215 }
216
217
218 void QtUi::closeNotification(uint notificationId)
219 {
220     QList<AbstractNotificationBackend::Notification>::iterator i = _notifications.begin();
221     while (i != _notifications.end()) {
222         if (i->notificationId == notificationId) {
223             foreach(AbstractNotificationBackend *backend, _notificationBackends)
224             backend->close(notificationId);
225             i = _notifications.erase(i);
226         }
227         else ++i;
228     }
229 }
230
231
232 void QtUi::closeNotifications(BufferId bufferId)
233 {
234     QList<AbstractNotificationBackend::Notification>::iterator i = _notifications.begin();
235     while (i != _notifications.end()) {
236         if (!bufferId.isValid() || i->bufferId == bufferId) {
237             foreach(AbstractNotificationBackend *backend, _notificationBackends)
238             backend->close(i->notificationId);
239             i = _notifications.erase(i);
240         }
241         else ++i;
242     }
243 }
244
245
246 const QList<AbstractNotificationBackend::Notification> &QtUi::activeNotifications()
247 {
248     return _notifications;
249 }
250
251
252 void QtUi::notificationActivated(uint notificationId)
253 {
254     if (notificationId != 0) {
255         QList<AbstractNotificationBackend::Notification>::iterator i = _notifications.begin();
256         while (i != _notifications.end()) {
257             if (i->notificationId == notificationId) {
258                 BufferId bufId = i->bufferId;
259                 if (bufId.isValid())
260                     Client::bufferModel()->switchToBuffer(bufId);
261                 break;
262             }
263             ++i;
264         }
265     }
266     closeNotification(notificationId);
267
268     activateMainWidget();
269 }
270
271
272 void QtUi::bufferMarkedAsRead(BufferId bufferId)
273 {
274     if (bufferId.isValid()) {
275         closeNotifications(bufferId);
276     }
277 }
278
279
280 std::vector<std::pair<QString, QString>> QtUi::availableIconThemes() const
281 {
282     //: Supported icon theme names
283     static const std::vector<std::pair<QString, QString>> supported {
284         { "breeze", tr("Breeze") },
285         { "breeze-dark", tr("Breeze Dark") },
286 #ifdef WITH_OXYGEN_ICONS
287         { "oxygen", tr("Oxygen") }
288 #endif
289     };
290
291     std::vector<std::pair<QString, QString>> result;
292     for (auto &&themePair : supported) {
293         for (auto &&dir : QIcon::themeSearchPaths()) {
294             if (QFileInfo{dir + "/" + themePair.first + "/index.theme"}.exists()) {
295                 result.push_back(themePair);
296                 break;
297             }
298         }
299     }
300
301     return result;
302 }
303
304
305 QString QtUi::systemIconTheme() const
306 {
307     return _systemIconTheme;
308 }
309
310
311 void QtUi::setupIconTheme()
312 {
313     // Add paths to our own icon sets to the theme search paths
314     QStringList themePaths = QIcon::themeSearchPaths();
315     themePaths.removeAll(":/icons");  // this should come last
316     for (auto &&dataDir : Quassel::dataDirPaths()) {
317         QString iconDir{dataDir + "icons"};
318         if (QFileInfo{iconDir}.isDir()) {
319             themePaths << iconDir;
320         }
321     }
322     themePaths << ":/icons";
323     QIcon::setThemeSearchPaths(themePaths);
324
325     refreshIconTheme();
326 }
327
328
329 void QtUi::refreshIconTheme()
330 {
331     // List of available fallback themes
332     QStringList availableThemes;
333     for (auto &&themePair : availableIconThemes()) {
334         availableThemes << themePair.first;
335     }
336
337     if (availableThemes.isEmpty()) {
338         // We could probably introduce a more sophisticated fallback handling, such as putting the "most important" icons into hicolor,
339         // but this just gets complex for no good reason. We really rely on a supported theme to be installed, if not system-wide, then
340         // as part of the Quassel installation (which is enabled by default anyway).
341         qWarning() << tr("No supported icon theme installed, you'll lack icons! Supported are the KDE/Plasma themes Breeze, Breeze Dark and Oxygen.");
342         return;
343     }
344
345     UiStyleSettings s;
346     QString fallbackTheme{s.value("Icons/FallbackTheme").toString()};
347
348     if (fallbackTheme.isEmpty() || !availableThemes.contains(fallbackTheme)) {
349         if (availableThemes.contains(_systemIconTheme)) {
350             fallbackTheme = _systemIconTheme;
351         }
352         else {
353             fallbackTheme = availableThemes.first();
354         }
355     }
356
357     if (_systemIconTheme.isEmpty() || _systemIconTheme == fallbackTheme || s.value("Icons/OverrideSystemTheme", true).toBool()) {
358         // We have a valid fallback theme and want to override the system theme (if it's even defined), so we're basically done
359         QIcon::setThemeName(fallbackTheme);
360         emit iconThemeRefreshed();
361         return;
362     }
363
364 #if QT_VERSION >= 0x050000
365     // At this point, we have a system theme that we don't want to override, but that may not contain all
366     // required icons.
367     // We create a dummy theme that inherits first from the system theme, then from the supported fallback.
368     // This rather ugly hack allows us to inject the fallback into the inheritance chain, so non-standard
369     // icons missing in the system theme will be filled in by the fallback.
370     // Since we can't get notified when the system theme changes, this means that a restart may be required
371     // to apply a theme change... but you can't have everything, I guess.
372     if (!_dummyThemeDir) {
373         _dummyThemeDir.reset(new QTemporaryDir{});
374         if (!_dummyThemeDir->isValid() || !QDir{_dummyThemeDir->path()}.mkpath("icons/quassel-icon-proxy/apps/32")) {
375             qWarning() << "Could not create temporary directory for proxying the system icon theme, using fallback";
376             QIcon::setThemeName(fallbackTheme);
377             emit iconThemeRefreshed();
378             return;
379         }
380         // Add this to XDG_DATA_DIRS, otherwise KIconLoader complains
381         auto xdgDataDirs = qgetenv("XDG_DATA_DIRS");
382         if (!xdgDataDirs.isEmpty())
383             xdgDataDirs += ":";
384         xdgDataDirs += _dummyThemeDir->path();
385         qputenv("XDG_DATA_DIRS", xdgDataDirs);
386
387         QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << _dummyThemeDir->path() + "/icons");
388     }
389
390     QFile indexFile{_dummyThemeDir->path() + "/icons/quassel-icon-proxy/index.theme"};
391     if (!indexFile.open(QFile::WriteOnly|QFile::Truncate)) {
392         qWarning() << "Could not create index file for proxying the system icon theme, using fallback";
393         QIcon::setThemeName(fallbackTheme);
394         emit iconThemeRefreshed();
395         return;
396     }
397
398     // Write a dummy index file that is sufficient to make QIconLoader happy
399     auto indexContents = QString{
400             "[Icon Theme]\n"
401             "Name=quassel-icon-proxy\n"
402             "Inherits=%1,%2\n"
403             "Directories=apps/32\n"
404             "[apps/32]\nSize=32\nType=Fixed\n"
405     }.arg(_systemIconTheme, fallbackTheme);
406     if (indexFile.write(indexContents.toLatin1()) < 0) {
407         qWarning() << "Could not write index file for proxying the system icon theme, using fallback";
408         QIcon::setThemeName(fallbackTheme);
409         emit iconThemeRefreshed();
410         return;
411     }
412     indexFile.close();
413     QIcon::setThemeName("quassel-icon-proxy");
414 #else
415     // Qt4 doesn't support QTemporaryDir. Since it's deprecated and slated to be removed soon anyway, we don't bother
416     // writing a replacement and simply don't support not overriding the system theme.
417     QIcon::setThemeName(fallbackTheme);
418     emit iconThemeRefreshed();
419 #endif
420 }