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