1 /*
2     Copyright © 2014-2019 by The qTox Project Contributors
3 
4     This file is part of qTox, a Qt-based graphical interface for Tox.
5 
6     qTox is libre software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     qTox is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with qTox.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 
21 #include "nexus.h"
22 #include "persistence/settings.h"
23 #include "src/core/core.h"
24 #include "src/core/coreav.h"
25 #include "src/model/groupinvite.h"
26 #include "src/model/status.h"
27 #include "src/persistence/profile.h"
28 #include "src/widget/widget.h"
29 #include "video/camerasource.h"
30 #include "widget/gui.h"
31 #include "widget/loginscreen.h"
32 #include <QApplication>
33 #include <QCommandLineParser>
34 #include <QDebug>
35 #include <QDesktopWidget>
36 #include <QThread>
37 #include <cassert>
38 #include <src/audio/audio.h>
39 #include <vpx/vpx_image.h>
40 
41 #ifdef Q_OS_MAC
42 #include <QActionGroup>
43 #include <QMenuBar>
44 #include <QSignalMapper>
45 #include <QWindow>
46 #endif
47 
48 /**
49  * @class Nexus
50  *
51  * This class is in charge of connecting various systems together
52  * and forwarding signals appropriately to the right objects,
53  * it is in charge of starting the GUI and the Core.
54  */
55 
Q_DECLARE_OPAQUE_POINTER(ToxAV *)56 Q_DECLARE_OPAQUE_POINTER(ToxAV*)
57 
58 static Nexus* nexus{nullptr};
59 
Nexus(QObject * parent)60 Nexus::Nexus(QObject* parent)
61     : QObject(parent)
62     , profile{nullptr}
63     , widget{nullptr}
64 {}
65 
~Nexus()66 Nexus::~Nexus()
67 {
68     delete widget;
69     widget = nullptr;
70     delete profile;
71     profile = nullptr;
72     emit saveGlobal();
73 #ifdef Q_OS_MAC
74     delete globalMenuBar;
75 #endif
76 }
77 
78 /**
79  * @brief Sets up invariants and calls showLogin
80  *
81  * Hides the login screen and shows the GUI for the given profile.
82  * Will delete the current GUI, if it exists.
83  */
start()84 void Nexus::start()
85 {
86     qDebug() << "Starting up";
87 
88     // Setup the environment
89     qRegisterMetaType<Status::Status>("Status::Status");
90     qRegisterMetaType<vpx_image>("vpx_image");
91     qRegisterMetaType<uint8_t>("uint8_t");
92     qRegisterMetaType<uint16_t>("uint16_t");
93     qRegisterMetaType<uint32_t>("uint32_t");
94     qRegisterMetaType<const int16_t*>("const int16_t*");
95     qRegisterMetaType<int32_t>("int32_t");
96     qRegisterMetaType<int64_t>("int64_t");
97     qRegisterMetaType<size_t>("size_t");
98     qRegisterMetaType<QPixmap>("QPixmap");
99     qRegisterMetaType<Profile*>("Profile*");
100     qRegisterMetaType<ToxAV*>("ToxAV*");
101     qRegisterMetaType<ToxFile>("ToxFile");
102     qRegisterMetaType<ToxFile::FileDirection>("ToxFile::FileDirection");
103     qRegisterMetaType<std::shared_ptr<VideoFrame>>("std::shared_ptr<VideoFrame>");
104     qRegisterMetaType<ToxPk>("ToxPk");
105     qRegisterMetaType<ToxId>("ToxId");
106     qRegisterMetaType<ToxPk>("GroupId");
107     qRegisterMetaType<ToxPk>("ContactId");
108     qRegisterMetaType<GroupInvite>("GroupInvite");
109     qRegisterMetaType<ReceiptNum>("ReceiptNum");
110     qRegisterMetaType<RowId>("RowId");
111 
112     qApp->setQuitOnLastWindowClosed(false);
113 
114 #ifdef Q_OS_MAC
115     // TODO: still needed?
116     globalMenuBar = new QMenuBar(0);
117     dockMenu = new QMenu(globalMenuBar);
118 
119     viewMenu = globalMenuBar->addMenu(QString());
120 
121     windowMenu = globalMenuBar->addMenu(QString());
122     globalMenuBar->addAction(windowMenu->menuAction());
123 
124     fullscreenAction = viewMenu->addAction(QString());
125     fullscreenAction->setShortcut(QKeySequence::FullScreen);
126     connect(fullscreenAction, &QAction::triggered, this, &Nexus::toggleFullscreen);
127 
128     minimizeAction = windowMenu->addAction(QString());
129     minimizeAction->setShortcut(Qt::CTRL + Qt::Key_M);
130     connect(minimizeAction, &QAction::triggered, [this]() {
131         minimizeAction->setEnabled(false);
132         QApplication::focusWindow()->showMinimized();
133     });
134 
135     windowMenu->addSeparator();
136     frontAction = windowMenu->addAction(QString());
137     connect(frontAction, &QAction::triggered, this, &Nexus::bringAllToFront);
138 
139     QAction* quitAction = new QAction(globalMenuBar);
140     quitAction->setMenuRole(QAction::QuitRole);
141     connect(quitAction, &QAction::triggered, qApp, &QApplication::quit);
142 
143     retranslateUi();
144 #endif
145     showMainGUI();
146 }
147 
148 /**
149  * @brief Hides the main GUI, delete the profile, and shows the login screen
150  */
showLogin(const QString & profileName)151 int Nexus::showLogin(const QString& profileName)
152 {
153     delete widget;
154     widget = nullptr;
155 
156     delete profile;
157     profile = nullptr;
158 
159     LoginScreen loginScreen{profileName};
160     connectLoginScreen(loginScreen);
161 
162     // TODO(kriby): Move core out of profile
163     // This is awkward because the core is in the profile
164     // The connection order ensures profile will be ready for bootstrap for now
165     connect(this, &Nexus::currentProfileChanged, this, &Nexus::bootstrapWithProfile);
166     int returnval = loginScreen.exec();
167     if (returnval == QDialog::Rejected) {
168         // Kriby: This will terminate the main application loop, necessary until we refactor
169         // away the split startup/return to login behavior.
170         qApp->quit();
171     }
172     disconnect(this, &Nexus::currentProfileChanged, this, &Nexus::bootstrapWithProfile);
173     return returnval;
174 }
175 
bootstrapWithProfile(Profile * p)176 void Nexus::bootstrapWithProfile(Profile* p)
177 {
178     // kriby: This is a hack until a proper controller is written
179 
180     profile = p;
181 
182     if (profile) {
183         audioControl = std::unique_ptr<IAudioControl>(Audio::makeAudio(*settings));
184         assert(audioControl != nullptr);
185         profile->getCore()->getAv()->setAudio(*audioControl);
186         start();
187     }
188 }
189 
setSettings(Settings * settings)190 void Nexus::setSettings(Settings* settings)
191 {
192     if (this->settings) {
193         QObject::disconnect(this, &Nexus::saveGlobal, this->settings, &Settings::saveGlobal);
194     }
195     this->settings = settings;
196     if (this->settings) {
197         QObject::connect(this, &Nexus::saveGlobal, this->settings, &Settings::saveGlobal);
198     }
199 }
200 
connectLoginScreen(const LoginScreen & loginScreen)201 void Nexus::connectLoginScreen(const LoginScreen& loginScreen)
202 {
203     // TODO(kriby): Move connect sequences to a controller class object instead
204 
205     // Nexus -> LoginScreen
206     QObject::connect(this, &Nexus::profileLoaded, &loginScreen, &LoginScreen::onProfileLoaded);
207     QObject::connect(this, &Nexus::profileLoadFailed, &loginScreen, &LoginScreen::onProfileLoadFailed);
208     // LoginScreen -> Nexus
209     QObject::connect(&loginScreen, &LoginScreen::createNewProfile, this, &Nexus::onCreateNewProfile);
210     QObject::connect(&loginScreen, &LoginScreen::loadProfile, this, &Nexus::onLoadProfile);
211     // LoginScreen -> Settings
212     QObject::connect(&loginScreen, &LoginScreen::autoLoginChanged, settings, &Settings::setAutoLogin);
213     QObject::connect(&loginScreen, &LoginScreen::autoLoginChanged, settings, &Settings::saveGlobal);
214     // Settings -> LoginScreen
215     QObject::connect(settings, &Settings::autoLoginChanged, &loginScreen,
216                      &LoginScreen::onAutoLoginChanged);
217 }
218 
showMainGUI()219 void Nexus::showMainGUI()
220 {
221     // TODO(kriby): Rewrite as view-model connect sequence only, add to a controller class object
222     assert(profile);
223 
224     // Create GUI
225     widget = new Widget(*audioControl);
226 
227     // Start GUI
228     widget->init();
229     GUI::getInstance();
230 
231     // Zetok protection
232     // There are small instants on startup during which no
233     // profile is loaded but the GUI could still receive events,
234     // e.g. between two modal windows. Disable the GUI to prevent that.
235     GUI::setEnabled(false);
236 
237     // Connections
238     connect(profile, &Profile::selfAvatarChanged, widget, &Widget::onSelfAvatarLoaded);
239 
240     connect(profile, &Profile::coreChanged, widget, &Widget::onCoreChanged);
241 
242     connect(profile, &Profile::failedToStart, widget, &Widget::onFailedToStartCore,
243             Qt::BlockingQueuedConnection);
244 
245     connect(profile, &Profile::badProxy, widget, &Widget::onBadProxyCore, Qt::BlockingQueuedConnection);
246 
247     profile->startCore();
248 
249     GUI::setEnabled(true);
250 }
251 
252 /**
253  * @brief Returns the singleton instance.
254  */
getInstance()255 Nexus& Nexus::getInstance()
256 {
257     if (!nexus)
258         nexus = new Nexus;
259 
260     return *nexus;
261 }
262 
destroyInstance()263 void Nexus::destroyInstance()
264 {
265     delete nexus;
266     nexus = nullptr;
267 }
268 
269 /**
270  * @brief Get core instance.
271  * @return nullptr if not started, core instance otherwise.
272  */
getCore()273 Core* Nexus::getCore()
274 {
275     Nexus& nexus = getInstance();
276     if (!nexus.profile)
277         return nullptr;
278 
279     return nexus.profile->getCore();
280 }
281 
282 /**
283  * @brief Get current user profile.
284  * @return nullptr if not started, profile otherwise.
285  */
getProfile()286 Profile* Nexus::getProfile()
287 {
288     return getInstance().profile;
289 }
290 
291 /**
292  * @brief Creates a new profile and replaces the current one.
293  * @param name New username
294  * @param pass New password
295  */
onCreateNewProfile(const QString & name,const QString & pass)296 void Nexus::onCreateNewProfile(const QString& name, const QString& pass)
297 {
298     setProfile(Profile::createProfile(name, parser, pass));
299     parser = nullptr; // only apply cmdline proxy settings once
300 }
301 
302 /**
303  * Loads an existing profile and replaces the current one.
304  */
onLoadProfile(const QString & name,const QString & pass)305 void Nexus::onLoadProfile(const QString& name, const QString& pass)
306 {
307     setProfile(Profile::loadProfile(name, parser, pass));
308     parser = nullptr; // only apply cmdline proxy settings once
309 }
310 /**
311  * Changes the loaded profile and notifies listeners.
312  * @param p
313  */
setProfile(Profile * p)314 void Nexus::setProfile(Profile* p)
315 {
316     if (!p) {
317         emit profileLoadFailed();
318         // Warnings are issued during respective createNew/load calls
319         return;
320     } else {
321         emit profileLoaded();
322     }
323 
324     emit currentProfileChanged(p);
325 }
326 
setParser(QCommandLineParser * parser)327 void Nexus::setParser(QCommandLineParser* parser)
328 {
329     this->parser = parser;
330 }
331 
332 /**
333  * @brief Get desktop GUI widget.
334  * @return nullptr if not started, desktop widget otherwise.
335  */
getDesktopGUI()336 Widget* Nexus::getDesktopGUI()
337 {
338     return getInstance().widget;
339 }
340 
341 #ifdef Q_OS_MAC
retranslateUi()342 void Nexus::retranslateUi()
343 {
344     viewMenu->menuAction()->setText(tr("View", "OS X Menu bar"));
345     windowMenu->menuAction()->setText(tr("Window", "OS X Menu bar"));
346     minimizeAction->setText(tr("Minimize", "OS X Menu bar"));
347     frontAction->setText((tr("Bring All to Front", "OS X Menu bar")));
348 }
349 
onWindowStateChanged(Qt::WindowStates state)350 void Nexus::onWindowStateChanged(Qt::WindowStates state)
351 {
352     minimizeAction->setEnabled(QApplication::activeWindow() != nullptr);
353 
354     if (QApplication::activeWindow() != nullptr && sender() == QApplication::activeWindow()) {
355         if (state & Qt::WindowFullScreen)
356             minimizeAction->setEnabled(false);
357 
358         if (state & Qt::WindowFullScreen)
359             fullscreenAction->setText(tr("Exit Fullscreen"));
360         else
361             fullscreenAction->setText(tr("Enter Fullscreen"));
362 
363         updateWindows();
364     }
365 
366     updateWindowsStates();
367 }
368 
updateWindows()369 void Nexus::updateWindows()
370 {
371     updateWindowsArg(nullptr);
372 }
373 
updateWindowsArg(QWindow * closedWindow)374 void Nexus::updateWindowsArg(QWindow* closedWindow)
375 {
376     QWindowList windowList = QApplication::topLevelWindows();
377     delete windowActions;
378     windowActions = new QActionGroup(this);
379 
380     windowMenu->addSeparator();
381 
382     QAction* dockLast;
383     if (!dockMenu->actions().isEmpty())
384         dockLast = dockMenu->actions().first();
385     else
386         dockLast = nullptr;
387 
388     QWindow* activeWindow;
389 
390     if (QApplication::activeWindow())
391         activeWindow = QApplication::activeWindow()->windowHandle();
392     else
393         activeWindow = nullptr;
394 
395     for (int i = 0; i < windowList.size(); ++i) {
396         if (closedWindow == windowList[i])
397             continue;
398 
399         QAction* action = windowActions->addAction(windowList[i]->title());
400         action->setCheckable(true);
401         action->setChecked(windowList[i] == activeWindow);
402         connect(action, &QAction::triggered, [=] { onOpenWindow(windowList[i]); });
403         windowMenu->addAction(action);
404         dockMenu->insertAction(dockLast, action);
405     }
406 
407     if (dockLast && !dockLast->isSeparator())
408         dockMenu->insertSeparator(dockLast);
409 }
410 
updateWindowsClosed()411 void Nexus::updateWindowsClosed()
412 {
413     updateWindowsArg(static_cast<QWidget*>(sender())->windowHandle());
414 }
415 
updateWindowsStates()416 void Nexus::updateWindowsStates()
417 {
418     bool exists = false;
419     QWindowList windowList = QApplication::topLevelWindows();
420 
421     for (QWindow* window : windowList) {
422         if (!(window->windowState() & Qt::WindowMinimized)) {
423             exists = true;
424             break;
425         }
426     }
427 
428     frontAction->setEnabled(exists);
429 }
430 
onOpenWindow(QObject * object)431 void Nexus::onOpenWindow(QObject* object)
432 {
433     QWindow* window = static_cast<QWindow*>(object);
434 
435     if (window->windowState() & QWindow::Minimized)
436         window->showNormal();
437 
438     window->raise();
439     window->requestActivate();
440 }
441 
toggleFullscreen()442 void Nexus::toggleFullscreen()
443 {
444     QWidget* window = QApplication::activeWindow();
445 
446     if (window->isFullScreen())
447         window->showNormal();
448     else
449         window->showFullScreen();
450 }
451 
bringAllToFront()452 void Nexus::bringAllToFront()
453 {
454     QWindowList windowList = QApplication::topLevelWindows();
455     QWindow* focused = QApplication::focusWindow();
456 
457     for (QWindow* window : windowList)
458         window->raise();
459 
460     focused->raise();
461 }
462 #endif
463