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