systray.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953
  1. /*
  2. * Copyright (C) by Cédric Bellegarde <gnumdk@gmail.com>
  3. *
  4. * This program is free software; you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation; either version 2 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  11. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  12. * for more details.
  13. */
  14. #include "accountmanager.h"
  15. #include "systray.h"
  16. #include "theme.h"
  17. #include "config.h"
  18. #include "common/utility.h"
  19. #include "tray/svgimageprovider.h"
  20. #include "tray/usermodel.h"
  21. #include "wheelhandler.h"
  22. #include "tray/trayimageprovider.h"
  23. #include "configfile.h"
  24. #include "accessmanager.h"
  25. #include "callstatechecker.h"
  26. #include <QCursor>
  27. #include <QGuiApplication>
  28. #include <QQmlApplicationEngine>
  29. #include <QQmlContext>
  30. #include <QQuickWindow>
  31. #include <QVariantMap>
  32. #include <QScreen>
  33. #include <QMenu>
  34. #include <QGuiApplication>
  35. #ifdef USE_FDO_NOTIFICATIONS
  36. #include <QDBusConnection>
  37. #include <QDBusInterface>
  38. #include <QDBusMessage>
  39. #include <QDBusPendingCall>
  40. #define NOTIFICATIONS_SERVICE "org.freedesktop.Notifications"
  41. #define NOTIFICATIONS_PATH "/org/freedesktop/Notifications"
  42. #define NOTIFICATIONS_IFACE "org.freedesktop.Notifications"
  43. #endif
  44. namespace OCC {
  45. Q_LOGGING_CATEGORY(lcSystray, "nextcloud.gui.systray")
  46. Systray *Systray::_instance = nullptr;
  47. Systray *Systray::instance()
  48. {
  49. if (!_instance) {
  50. _instance = new Systray();
  51. }
  52. return _instance;
  53. }
  54. void Systray::setTrayEngine(QQmlApplicationEngine *trayEngine)
  55. {
  56. _trayEngine = trayEngine;
  57. _trayEngine->setNetworkAccessManagerFactory(&_accessManagerFactory);
  58. _trayEngine->addImportPath("qrc:/qml/theme");
  59. _trayEngine->addImageProvider("avatars", new ImageProvider);
  60. _trayEngine->addImageProvider(QLatin1String("svgimage-custom-color"), new OCC::Ui::SvgImageProvider);
  61. _trayEngine->addImageProvider(QLatin1String("tray-image-provider"), new TrayImageProvider);
  62. }
  63. Systray::Systray()
  64. : QSystemTrayIcon(nullptr)
  65. {
  66. #if defined(Q_OS_MACOS) && defined(BUILD_OWNCLOUD_OSX_BUNDLE)
  67. setUserNotificationCenterDelegate();
  68. checkNotificationAuth(MacNotificationAuthorizationOptions::Default); // No provisional auth, ask user explicitly first time
  69. registerNotificationCategories(QString(tr("Download")));
  70. #elif !defined(Q_OS_MACOS)
  71. connect(AccountManager::instance(), &AccountManager::accountAdded,
  72. this, &Systray::setupContextMenu);
  73. connect(AccountManager::instance(), &AccountManager::accountRemoved,
  74. this, &Systray::setupContextMenu);
  75. setupContextMenu();
  76. #endif
  77. connect(UserModel::instance(), &UserModel::currentUserChanged,
  78. this, &Systray::slotCurrentUserChanged);
  79. connect(UserModel::instance(), &UserModel::addAccount,
  80. this, &Systray::openAccountWizard);
  81. #if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
  82. connect(AccountManager::instance(), &AccountManager::accountAdded,
  83. this, [this]{ showWindow(); });
  84. #else
  85. // Since the positioning of the QSystemTrayIcon is borked on non-Windows and non-macOS desktop environments,
  86. // we hardcode the position of the tray to be in the center when we add a new account from somewhere like
  87. // the wizard. Otherwise with the conventional method we end up with the tray appearing wherever the cursor
  88. // is placed
  89. connect(AccountManager::instance(), &AccountManager::accountAdded,
  90. this, [this]{ showWindow(WindowPosition::Center); });
  91. #endif
  92. }
  93. void Systray::create()
  94. {
  95. if (_trayEngine) {
  96. if (!AccountManager::instance()->accounts().isEmpty()) {
  97. _trayEngine->rootContext()->setContextProperty("activityModel", UserModel::instance()->currentActivityModel());
  98. }
  99. QQmlComponent trayWindowComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/tray/Window.qml"));
  100. if(trayWindowComponent.isError()) {
  101. qCWarning(lcSystray) << trayWindowComponent.errorString();
  102. } else {
  103. _trayWindow.reset(qobject_cast<QQuickWindow*>(trayWindowComponent.create()));
  104. }
  105. }
  106. hideWindow();
  107. emit activated(QSystemTrayIcon::ActivationReason::Unknown);
  108. const auto folderMap = FolderMan::instance()->map();
  109. for (const auto *folder : folderMap) {
  110. if (!folder->syncPaused()) {
  111. _syncIsPaused = false;
  112. break;
  113. }
  114. }
  115. }
  116. void Systray::showWindow(WindowPosition position)
  117. {
  118. if(isOpen() || !_trayWindow) {
  119. return;
  120. }
  121. if(position == WindowPosition::Center) {
  122. positionWindowAtScreenCenter(_trayWindow.data());
  123. } else {
  124. positionWindowAtTray(_trayWindow.data());
  125. }
  126. _trayWindow->show();
  127. _trayWindow->raise();
  128. _trayWindow->requestActivate();
  129. setIsOpen(true);
  130. UserModel::instance()->fetchCurrentActivityModel();
  131. }
  132. void Systray::hideWindow()
  133. {
  134. if(!isOpen() || !_trayWindow) {
  135. return;
  136. }
  137. _trayWindow->hide();
  138. setIsOpen(false);
  139. }
  140. void Systray::setupContextMenu()
  141. {
  142. const auto oldContextMenu = _contextMenu.data();
  143. // If we delete the old QMenu before setting the new one the client will crash on GNOME.
  144. // Let's delete it once this method is over
  145. if(oldContextMenu) {
  146. oldContextMenu->deleteLater();
  147. }
  148. _contextMenu = new QMenu();
  149. // NOTE: for reasons unclear, setting the the new menu after adding all the actions
  150. // will not work on GNOME, as the old menu will not be correctly replaced.
  151. setContextMenu(_contextMenu);
  152. if (AccountManager::instance()->accounts().isEmpty()) {
  153. _contextMenu->addAction(tr("Add account"), this, &Systray::openAccountWizard);
  154. } else {
  155. _contextMenu->addAction(tr("Open main dialog"), this, [this]{ showWindow(); });
  156. }
  157. auto pauseAction = _contextMenu->addAction(tr("Pause sync"), this, &Systray::slotPauseAllFolders);
  158. auto resumeAction = _contextMenu->addAction(tr("Resume sync"), this, &Systray::slotUnpauseAllFolders);
  159. _contextMenu->addAction(tr("Settings"), this, &Systray::openSettings);
  160. _contextMenu->addAction(tr("Help"), this, &Systray::openHelp);
  161. _contextMenu->addAction(tr("Exit %1").arg(Theme::instance()->appNameGUI()), this, &Systray::shutdown);
  162. connect(_contextMenu, &QMenu::aboutToShow, [=] {
  163. const auto folders = FolderMan::instance()->map();
  164. const auto allPaused = std::all_of(std::cbegin(folders), std::cend(folders), [](Folder *f) { return f->syncPaused(); });
  165. const auto pauseText = folders.size() > 1 ? tr("Pause sync for all") : tr("Pause sync");
  166. pauseAction->setText(pauseText);
  167. pauseAction->setVisible(!allPaused);
  168. pauseAction->setEnabled(!allPaused);
  169. const auto anyPaused = std::any_of(std::cbegin(folders), std::cend(folders), [](Folder *f) { return f->syncPaused(); });
  170. const auto resumeText = folders.size() > 1 ? tr("Resume sync for all") : tr("Resume sync");
  171. resumeAction->setText(resumeText);
  172. resumeAction->setVisible(anyPaused);
  173. resumeAction->setEnabled(anyPaused);
  174. });
  175. }
  176. void Systray::destroyDialog(QQuickWindow *dialog) const
  177. {
  178. dialog->destroy();
  179. dialog->deleteLater();
  180. }
  181. void Systray::createCallDialog(const Activity &callNotification, const AccountStatePtr accountState)
  182. {
  183. qCDebug(lcSystray) << "Starting a new call dialog for notification with id: " << callNotification._id << "with text: " << callNotification._subject;
  184. if (_trayEngine && !_callsAlreadyNotified.contains(callNotification._id)) {
  185. const QVariantMap talkNotificationData{
  186. {"conversationToken", callNotification._talkNotificationData.conversationToken},
  187. {"messageId", callNotification._talkNotificationData.messageId},
  188. {"messageSent", callNotification._talkNotificationData.messageSent},
  189. {"userAvatar", callNotification._talkNotificationData.userAvatar},
  190. };
  191. QVariantList links;
  192. for(const auto &link : callNotification._links) {
  193. links.append(QVariantMap{
  194. {"imageSource", link._imageSource},
  195. {"imageSourceHovered", link._imageSourceHovered},
  196. {"label", link._label},
  197. {"link", link._link},
  198. {"primary", link._primary},
  199. {"verb", link._verb},
  200. });
  201. }
  202. const QVariantMap initialProperties{
  203. {"accountState", QVariant::fromValue(accountState.data())},
  204. {"talkNotificationData", talkNotificationData},
  205. {"links", links},
  206. {"subject", callNotification._subject},
  207. {"link", callNotification._link},
  208. };
  209. const auto callDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/tray/CallNotificationDialog.qml"));
  210. if(callDialog->isError()) {
  211. qCWarning(lcSystray) << callDialog->errorString();
  212. return;
  213. }
  214. // This call dialog gets deallocated on close conditions
  215. // by a call from the QML side to the destroyDialog slot
  216. callDialog->createWithInitialProperties(initialProperties);
  217. _callsAlreadyNotified.insert(callNotification._id);
  218. }
  219. }
  220. void Systray::createEditFileLocallyLoadingDialog(const QString &fileName)
  221. {
  222. if (_editFileLocallyLoadingDialog) {
  223. return;
  224. }
  225. qCDebug(lcSystray) << "Opening a file local editing dialog...";
  226. const auto editFileLocallyLoadingDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/tray/EditFileLocallyLoadingDialog.qml"));
  227. if (editFileLocallyLoadingDialog->isError()) {
  228. qCWarning(lcSystray) << editFileLocallyLoadingDialog->errorString();
  229. return;
  230. }
  231. _editFileLocallyLoadingDialog = editFileLocallyLoadingDialog->createWithInitialProperties(QVariantMap{{QStringLiteral("fileName"), fileName}});
  232. }
  233. void Systray::destroyEditFileLocallyLoadingDialog()
  234. {
  235. if (!_editFileLocallyLoadingDialog) {
  236. return;
  237. }
  238. qCDebug(lcSystray) << "Closing a file local editing dialog...";
  239. _editFileLocallyLoadingDialog->deleteLater();
  240. _editFileLocallyLoadingDialog = nullptr;
  241. }
  242. void Systray::createResolveConflictsDialog(const OCC::ActivityList &allConflicts)
  243. {
  244. const auto conflictsDialog = std::make_unique<QQmlComponent>(_trayEngine, QStringLiteral("qrc:/qml/src/gui/ResolveConflictsDialog.qml"));
  245. const QVariantMap initialProperties{
  246. {"allConflicts", QVariant::fromValue(allConflicts)},
  247. };
  248. if(conflictsDialog->isError()) {
  249. qCWarning(lcSystray) << conflictsDialog->errorString();
  250. return;
  251. }
  252. // This call dialog gets deallocated on close conditions
  253. // by a call from the QML side to the destroyDialog slot
  254. auto dialog = QScopedPointer(conflictsDialog->createWithInitialProperties(initialProperties));
  255. if (!dialog) {
  256. return;
  257. }
  258. dialog->setParent(QGuiApplication::instance());
  259. auto dialogWindow = qobject_cast<QQuickWindow*>(dialog.data());
  260. if (!dialogWindow) {
  261. return;
  262. }
  263. dialogWindow->show();
  264. dialogWindow->raise();
  265. dialogWindow->requestActivate();
  266. dialog.take();
  267. }
  268. bool Systray::raiseDialogs()
  269. {
  270. return raiseFileDetailDialogs();
  271. }
  272. bool Systray::raiseFileDetailDialogs(const QString &localPath)
  273. {
  274. if(_fileDetailDialogs.empty()) {
  275. return false;
  276. }
  277. auto it = _fileDetailDialogs.begin();
  278. while (it != _fileDetailDialogs.end()) {
  279. const auto dialog = *it;
  280. auto nullDialog = dialog == nullptr;
  281. if (!nullDialog && !dialog->isVisible()) {
  282. destroyDialog(dialog);
  283. nullDialog = true;
  284. }
  285. if (!nullDialog && (localPath.isEmpty() || dialog->property("localPath").toString() == localPath)) {
  286. dialog->show();
  287. dialog->raise();
  288. dialog->requestActivate();
  289. ++it;
  290. continue;
  291. }
  292. it = _fileDetailDialogs.erase(it);
  293. continue;
  294. }
  295. // If it is empty then we have raised no dialogs, so return false (and viceversa)
  296. return !_fileDetailDialogs.empty();
  297. }
  298. void Systray::createFileDetailsDialog(const QString &localPath)
  299. {
  300. if (raiseFileDetailDialogs(localPath)) {
  301. qCDebug(lcSystray) << "Reopening an existing file details dialog for " << localPath;
  302. return;
  303. }
  304. qCDebug(lcSystray) << "Opening new file details dialog for " << localPath;
  305. if (!_trayEngine) {
  306. qCWarning(lcSystray) << "Could not open file details dialog for" << localPath << "as no tray engine was available";
  307. return;
  308. }
  309. const auto folder = FolderMan::instance()->folderForPath(localPath);
  310. if (!folder) {
  311. qCWarning(lcSystray) << "Could not open file details dialog for" << localPath << "no responsible folder found";
  312. return;
  313. }
  314. const QVariantMap initialProperties{
  315. {"accountState", QVariant::fromValue(folder->accountState())},
  316. {"localPath", localPath},
  317. };
  318. QQmlComponent fileDetailsDialog(_trayEngine, QStringLiteral("qrc:/qml/src/gui/filedetails/FileDetailsWindow.qml"));
  319. if (!fileDetailsDialog.isError()) {
  320. const auto createdDialog = fileDetailsDialog.createWithInitialProperties(initialProperties);
  321. const auto dialog = qobject_cast<QQuickWindow*>(createdDialog);
  322. if(!dialog) {
  323. qCWarning(lcSystray) << "File details dialog window resulted in creation of object that was not a window!";
  324. return;
  325. }
  326. _fileDetailDialogs.append(dialog);
  327. dialog->show();
  328. dialog->raise();
  329. dialog->requestActivate();
  330. } else {
  331. qCWarning(lcSystray) << fileDetailsDialog.errorString();
  332. }
  333. }
  334. void Systray::createShareDialog(const QString &localPath)
  335. {
  336. createFileDetailsDialog(localPath);
  337. Q_EMIT showFileDetailsPage(localPath, FileDetailsPage::Sharing);
  338. }
  339. void Systray::createFileActivityDialog(const QString &localPath)
  340. {
  341. createFileDetailsDialog(localPath);
  342. Q_EMIT showFileDetailsPage(localPath, FileDetailsPage::Activity);
  343. }
  344. void Systray::presentShareViewInTray(const QString &localPath)
  345. {
  346. const auto folder = FolderMan::instance()->folderForPath(localPath);
  347. if (!folder) {
  348. qCWarning(lcSystray) << "Could not open file details view in tray for" << localPath << "no responsible folder found";
  349. return;
  350. }
  351. qCDebug(lcSystray) << "Opening file details view in tray for " << localPath;
  352. Q_EMIT showFileDetails(folder->accountState(), localPath, FileDetailsPage::Sharing);
  353. }
  354. void Systray::slotCurrentUserChanged()
  355. {
  356. if (_trayEngine) {
  357. // Change ActivityModel
  358. _trayEngine->rootContext()->setContextProperty("activityModel", UserModel::instance()->currentActivityModel());
  359. }
  360. // Rebuild App list
  361. UserAppsModel::instance()->buildAppList();
  362. }
  363. void Systray::slotUnpauseAllFolders()
  364. {
  365. setPauseOnAllFoldersHelper(false);
  366. }
  367. void Systray::slotPauseAllFolders()
  368. {
  369. setPauseOnAllFoldersHelper(true);
  370. }
  371. void Systray::setPauseOnAllFoldersHelper(bool pause)
  372. {
  373. // For some reason we get the raw pointer from Folder::accountState()
  374. // that's why we need a list of raw pointers for the call to contains
  375. // later on...
  376. const auto accounts = [=] {
  377. const auto ptrList = AccountManager::instance()->accounts();
  378. auto result = QList<AccountState *>();
  379. result.reserve(ptrList.size());
  380. std::transform(std::cbegin(ptrList), std::cend(ptrList), std::back_inserter(result), [](const AccountStatePtr &account) {
  381. return account.data();
  382. });
  383. return result;
  384. }();
  385. const auto folders = FolderMan::instance()->map();
  386. for (auto f : folders) {
  387. if (accounts.contains(f->accountState())) {
  388. f->setSyncPaused(pause);
  389. if (pause) {
  390. f->slotTerminateSync();
  391. }
  392. }
  393. }
  394. }
  395. QString Systray::windowTitle() const
  396. {
  397. return Theme::instance()->appNameGUI();
  398. }
  399. bool Systray::useNormalWindow() const
  400. {
  401. if (!isSystemTrayAvailable()) {
  402. return true;
  403. }
  404. ConfigFile cfg;
  405. return cfg.showMainDialogAsNormalWindow();
  406. }
  407. bool Systray::isOpen() const
  408. {
  409. return _isOpen;
  410. }
  411. bool Systray::enableAddAccount() const
  412. {
  413. #if defined ENFORCE_SINGLE_ACCOUNT
  414. return AccountManager::instance()->accounts().isEmpty();
  415. #else
  416. return true;
  417. #endif
  418. }
  419. void Systray::setIsOpen(const bool isOpen)
  420. {
  421. _isOpen = isOpen;
  422. Q_EMIT isOpenChanged();
  423. }
  424. void Systray::showMessage(const QString &title, const QString &message, MessageIcon icon)
  425. {
  426. #ifdef USE_FDO_NOTIFICATIONS
  427. if (QDBusInterface(NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE).isValid()) {
  428. const QVariantMap hints = {{QStringLiteral("desktop-entry"), LINUX_APPLICATION_ID}};
  429. QList<QVariant> args = QList<QVariant>() << APPLICATION_NAME << quint32(0) << APPLICATION_ICON_NAME
  430. << title << message << QStringList() << hints << qint32(-1);
  431. QDBusMessage method = QDBusMessage::createMethodCall(NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE, "Notify");
  432. method.setArguments(args);
  433. QDBusConnection::sessionBus().asyncCall(method);
  434. } else
  435. #endif
  436. #if defined(Q_OS_MACOS) && defined(BUILD_OWNCLOUD_OSX_BUNDLE)
  437. if (canOsXSendUserNotification()) {
  438. sendOsXUserNotification(title, message);
  439. } else
  440. #endif
  441. {
  442. QSystemTrayIcon::showMessage(title, message, icon);
  443. }
  444. }
  445. void Systray::showUpdateMessage(const QString &title, const QString &message, const QUrl &webUrl)
  446. {
  447. #if defined(Q_OS_MACOS) && defined(BUILD_OWNCLOUD_OSX_BUNDLE)
  448. sendOsXUpdateNotification(title, message, webUrl);
  449. #else // TODO: Implement custom notifications (i.e. actionable) for other OSes
  450. Q_UNUSED(webUrl);
  451. showMessage(title, message);
  452. #endif
  453. }
  454. void Systray::showTalkMessage(const QString &title, const QString &message, const QString &token, const QString &replyTo, const AccountStatePtr &accountState)
  455. {
  456. #if defined(Q_OS_MACOS) && defined(BUILD_OWNCLOUD_OSX_BUNDLE)
  457. sendOsXTalkNotification(title, message, token, replyTo, accountState);
  458. #else // TODO: Implement custom notifications (i.e. actionable) for other OSes
  459. Q_UNUSED(replyTo)
  460. Q_UNUSED(token)
  461. Q_UNUSED(accountState)
  462. showMessage(title, message);
  463. #endif
  464. }
  465. void Systray::setToolTip(const QString &tip)
  466. {
  467. QSystemTrayIcon::setToolTip(tr("%1: %2").arg(Theme::instance()->appNameGUI(), tip));
  468. }
  469. bool Systray::syncIsPaused() const
  470. {
  471. return _syncIsPaused;
  472. }
  473. void Systray::setSyncIsPaused(const bool syncIsPaused)
  474. {
  475. _syncIsPaused = syncIsPaused;
  476. if (_syncIsPaused) {
  477. slotPauseAllFolders();
  478. } else {
  479. slotUnpauseAllFolders();
  480. }
  481. }
  482. /********************************************************************************************/
  483. /* Helper functions for cross-platform tray icon position and taskbar orientation detection */
  484. /********************************************************************************************/
  485. void Systray::positionWindowAtTray(QQuickWindow *window) const
  486. {
  487. if (!useNormalWindow()) {
  488. window->setScreen(currentScreen());
  489. const auto position = computeWindowPosition(window->width(), window->height());
  490. window->setPosition(position);
  491. }
  492. }
  493. void Systray::positionWindowAtScreenCenter(QQuickWindow *window) const
  494. {
  495. if(!useNormalWindow()) {
  496. window->setScreen(currentScreen());
  497. const QPoint windowAdjustment(window->geometry().width() / 2, window->geometry().height() / 2);
  498. const auto position = currentScreen()->virtualGeometry().center() - windowAdjustment;
  499. window->setPosition(position);
  500. }
  501. }
  502. void Systray::forceWindowInit(QQuickWindow *window) const
  503. {
  504. // HACK: At least on Windows, if the systray window is not shown at least once
  505. // it can prevent session handling to carry on properly, so we show/hide it here
  506. // this shouldn't flicker
  507. window->show();
  508. window->hide();
  509. #ifdef Q_OS_MAC
  510. // On macOS we need to designate the tray window as visible on all spaces and
  511. // at the menu bar level, otherwise showing it can cause the current spaces to
  512. // change, or the window could be obscured by another window that shouldn't
  513. // normally cover a menu.
  514. OCC::setTrayWindowLevelAndVisibleOnAllSpaces(window);
  515. #endif
  516. }
  517. void Systray::positionNotificationWindow(QQuickWindow *window) const
  518. {
  519. if (!useNormalWindow()) {
  520. window->setScreen(currentScreen());
  521. if(geometry().isValid()) {
  522. // On OSes where the QSystemTrayIcon geometry method isn't borked, we can actually figure out where the system tray is located
  523. // We can therefore use our normal routines
  524. const auto position = computeNotificationPosition(window->width(), window->height());
  525. window->setPosition(position);
  526. } else if (QProcessEnvironment::systemEnvironment().contains(QStringLiteral("XDG_CURRENT_DESKTOP")) &&
  527. (QProcessEnvironment::systemEnvironment().value(QStringLiteral("XDG_CURRENT_DESKTOP")).contains(QStringLiteral("GNOME")))) {
  528. // We can safely hardcode the top-right position for the notification when running GNOME
  529. const auto position = computeNotificationPosition(window->width(), window->height(), 0, NotificationPosition::TopRight);
  530. window->setPosition(position);
  531. } else {
  532. // For other DEs we play it safe and place the notification in the centre of the screen
  533. positionWindowAtScreenCenter(window);
  534. }
  535. // TODO: Get actual notification positions for the DEs
  536. }
  537. }
  538. QScreen *Systray::currentScreen() const
  539. {
  540. const auto screen = QGuiApplication::screenAt(QCursor::pos());
  541. if(screen) {
  542. return screen;
  543. }
  544. // Didn't find anything matching the cursor position,
  545. // falling back to the primary screen
  546. return QGuiApplication::primaryScreen();
  547. }
  548. Systray::TaskBarPosition Systray::taskbarOrientation() const
  549. {
  550. // macOS: Always on top
  551. #if defined(Q_OS_MACOS)
  552. return TaskBarPosition::Top;
  553. // Windows: Check registry for actual taskbar orientation
  554. #elif defined(Q_OS_WIN)
  555. auto taskbarPositionSubkey = QStringLiteral("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StuckRects3");
  556. if (!Utility::registryKeyExists(HKEY_CURRENT_USER, taskbarPositionSubkey)) {
  557. // Windows 7
  558. taskbarPositionSubkey = QStringLiteral("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StuckRects2");
  559. }
  560. if (!Utility::registryKeyExists(HKEY_CURRENT_USER, taskbarPositionSubkey)) {
  561. return TaskBarPosition::Bottom;
  562. }
  563. auto taskbarPosition = Utility::registryGetKeyValue(HKEY_CURRENT_USER, taskbarPositionSubkey, "Settings");
  564. switch (taskbarPosition.toInt()) {
  565. // Mapping windows binary value (0 = left, 1 = top, 2 = right, 3 = bottom) to qml logic (0 = bottom, 1 = left...)
  566. case 0:
  567. return TaskBarPosition::Left;
  568. case 1:
  569. return TaskBarPosition::Top;
  570. case 2:
  571. return TaskBarPosition::Right;
  572. case 3:
  573. return TaskBarPosition::Bottom;
  574. default:
  575. return TaskBarPosition::Bottom;
  576. }
  577. // Probably Linux
  578. #else
  579. const auto screenRect = currentScreenRect();
  580. const auto trayIconCenter = calcTrayIconCenter();
  581. const auto distBottom = screenRect.bottom() - trayIconCenter.y();
  582. const auto distRight = screenRect.right() - trayIconCenter.x();
  583. const auto distLeft = trayIconCenter.x() - screenRect.left();
  584. const auto distTop = trayIconCenter.y() - screenRect.top();
  585. const auto minDist = std::min({distRight, distTop, distBottom});
  586. if (minDist == distBottom) {
  587. return TaskBarPosition::Bottom;
  588. } else if (minDist == distLeft) {
  589. return TaskBarPosition::Left;
  590. } else if (minDist == distTop) {
  591. return TaskBarPosition::Top;
  592. } else {
  593. return TaskBarPosition::Right;
  594. }
  595. #endif
  596. }
  597. // TODO: Get real taskbar dimensions Linux as well
  598. QRect Systray::taskbarGeometry() const
  599. {
  600. #if defined(Q_OS_WIN)
  601. QRect tbRect = Utility::getTaskbarDimensions();
  602. //QML side expects effective pixels, convert taskbar dimensions if necessary
  603. auto pixelRatio = currentScreen()->devicePixelRatio();
  604. if (pixelRatio != 1) {
  605. tbRect.setHeight(tbRect.height() / pixelRatio);
  606. tbRect.setWidth(tbRect.width() / pixelRatio);
  607. }
  608. return tbRect;
  609. #elif defined(Q_OS_MACOS)
  610. const auto screenWidth = currentScreenRect().width();
  611. const auto statusBarHeight = static_cast<int>(OCC::menuBarThickness());
  612. return {0, 0, screenWidth, statusBarHeight};
  613. #else
  614. if (taskbarOrientation() == TaskBarPosition::Bottom || taskbarOrientation() == TaskBarPosition::Top) {
  615. auto screenWidth = currentScreenRect().width();
  616. return {0, 0, screenWidth, 32};
  617. } else {
  618. auto screenHeight = currentScreenRect().height();
  619. return {0, 0, 32, screenHeight};
  620. }
  621. #endif
  622. }
  623. QRect Systray::currentScreenRect() const
  624. {
  625. const auto screen = currentScreen();
  626. Q_ASSERT(screen);
  627. return screen->geometry();
  628. }
  629. QPoint Systray::computeWindowReferencePoint() const
  630. {
  631. constexpr auto spacing = 4;
  632. const auto trayIconCenter = calcTrayIconCenter();
  633. const auto taskbarRect = taskbarGeometry();
  634. const auto taskbarScreenEdge = taskbarOrientation();
  635. const auto screenRect = currentScreenRect();
  636. qCDebug(lcSystray) << "screenRect:" << screenRect;
  637. qCDebug(lcSystray) << "taskbarRect:" << taskbarRect;
  638. qCDebug(lcSystray) << "taskbarScreenEdge:" << taskbarScreenEdge;
  639. qCDebug(lcSystray) << "trayIconCenter:" << trayIconCenter;
  640. switch(taskbarScreenEdge) {
  641. case TaskBarPosition::Bottom:
  642. return {
  643. trayIconCenter.x(),
  644. screenRect.bottom() - taskbarRect.height() - spacing
  645. };
  646. case TaskBarPosition::Left:
  647. return {
  648. screenRect.left() + taskbarRect.width() + spacing,
  649. trayIconCenter.y()
  650. };
  651. case TaskBarPosition::Top:
  652. return {
  653. trayIconCenter.x(),
  654. screenRect.top() + taskbarRect.height() + spacing
  655. };
  656. case TaskBarPosition::Right:
  657. return {
  658. screenRect.right() - taskbarRect.width() - spacing,
  659. trayIconCenter.y()
  660. };
  661. }
  662. Q_UNREACHABLE();
  663. }
  664. QPoint Systray::computeNotificationReferencePoint(int spacing, NotificationPosition position) const
  665. {
  666. auto trayIconCenter = calcTrayIconCenter();
  667. auto taskbarScreenEdge = taskbarOrientation();
  668. auto taskbarRect = taskbarGeometry();
  669. const auto screenRect = currentScreenRect();
  670. if(position == NotificationPosition::TopLeft) {
  671. taskbarScreenEdge = TaskBarPosition::Top;
  672. trayIconCenter = QPoint(0, 0);
  673. taskbarRect = QRect(0, 0, screenRect.width(), 32);
  674. } else if(position == NotificationPosition::TopRight) {
  675. taskbarScreenEdge = TaskBarPosition::Top;
  676. trayIconCenter = QPoint(screenRect.width(), 0);
  677. taskbarRect = QRect(0, 0, screenRect.width(), 32);
  678. } else if(position == NotificationPosition::BottomLeft) {
  679. taskbarScreenEdge = TaskBarPosition::Bottom;
  680. trayIconCenter = QPoint(0, screenRect.height());
  681. taskbarRect = QRect(0, 0, screenRect.width(), 32);
  682. } else if(position == NotificationPosition::BottomRight) {
  683. taskbarScreenEdge = TaskBarPosition::Bottom;
  684. trayIconCenter = QPoint(screenRect.width(), screenRect.height());
  685. taskbarRect = QRect(0, 0, screenRect.width(), 32);
  686. }
  687. qCDebug(lcSystray) << "screenRect:" << screenRect;
  688. qCDebug(lcSystray) << "taskbarRect:" << taskbarRect;
  689. qCDebug(lcSystray) << "taskbarScreenEdge:" << taskbarScreenEdge;
  690. qCDebug(lcSystray) << "trayIconCenter:" << trayIconCenter;
  691. switch(taskbarScreenEdge) {
  692. case TaskBarPosition::Bottom:
  693. return {
  694. trayIconCenter.x() < screenRect.center().x() ? screenRect.left() + spacing : screenRect.right() - spacing,
  695. screenRect.bottom() - taskbarRect.height() - spacing
  696. };
  697. case TaskBarPosition::Left:
  698. return {
  699. screenRect.left() + taskbarRect.width() + spacing,
  700. trayIconCenter.y() < screenRect.center().y() ? screenRect.top() + spacing : screenRect.bottom() - spacing
  701. };
  702. case TaskBarPosition::Top:
  703. return {
  704. trayIconCenter.x() < screenRect.center().x() ? screenRect.left() + spacing : screenRect.right() - spacing,
  705. screenRect.top() + taskbarRect.height() + spacing
  706. };
  707. case TaskBarPosition::Right:
  708. return {
  709. screenRect.right() - taskbarRect.width() - spacing,
  710. trayIconCenter.y() < screenRect.center().y() ? screenRect.top() + spacing : screenRect.bottom() - spacing
  711. };
  712. }
  713. Q_UNREACHABLE();
  714. }
  715. QRect Systray::computeWindowRect(int spacing, const QPoint &topLeft, const QPoint &bottomRight) const
  716. {
  717. const auto screenRect = currentScreenRect();
  718. const auto rect = QRect(topLeft, bottomRight);
  719. auto offset = QPoint();
  720. if (rect.left() < screenRect.left()) {
  721. offset.setX(screenRect.left() - rect.left() + spacing);
  722. } else if (rect.right() > screenRect.right()) {
  723. offset.setX(screenRect.right() - rect.right() - spacing);
  724. }
  725. if (rect.top() < screenRect.top()) {
  726. offset.setY(screenRect.top() - rect.top() + spacing);
  727. } else if (rect.bottom() > screenRect.bottom()) {
  728. offset.setY(screenRect.bottom() - rect.bottom() - spacing);
  729. }
  730. return rect.translated(offset);
  731. }
  732. QPoint Systray::computeWindowPosition(int width, int height) const
  733. {
  734. constexpr auto spacing = 4;
  735. const auto referencePoint = computeWindowReferencePoint();
  736. const auto taskbarScreenEdge = taskbarOrientation();
  737. const auto screenRect = currentScreenRect();
  738. const auto topLeft = [=]() {
  739. switch(taskbarScreenEdge) {
  740. case TaskBarPosition::Bottom:
  741. return referencePoint - QPoint(width / 2, height);
  742. case TaskBarPosition::Left:
  743. return referencePoint;
  744. case TaskBarPosition::Top:
  745. return referencePoint - QPoint(width / 2, 0);
  746. case TaskBarPosition::Right:
  747. return referencePoint - QPoint(width, 0);
  748. }
  749. Q_UNREACHABLE();
  750. }();
  751. const auto bottomRight = topLeft + QPoint(width, height);
  752. const auto windowRect = computeWindowRect(spacing, topLeft, bottomRight);
  753. qCDebug(lcSystray) << "taskbarScreenEdge:" << taskbarScreenEdge;
  754. qCDebug(lcSystray) << "screenRect:" << screenRect;
  755. qCDebug(lcSystray) << "windowRect (reference)" << QRect(topLeft, bottomRight);
  756. qCDebug(lcSystray) << "windowRect (adjusted)" << windowRect;
  757. return windowRect.topLeft();
  758. }
  759. QPoint Systray::computeNotificationPosition(int width, int height, int spacing, NotificationPosition position) const
  760. {
  761. const auto referencePoint = computeNotificationReferencePoint(spacing, position);
  762. auto trayIconCenter = calcTrayIconCenter();
  763. auto taskbarScreenEdge = taskbarOrientation();
  764. const auto screenRect = currentScreenRect();
  765. if(position == NotificationPosition::TopLeft) {
  766. taskbarScreenEdge = TaskBarPosition::Top;
  767. trayIconCenter = QPoint(0, 0);
  768. } else if(position == NotificationPosition::TopRight) {
  769. taskbarScreenEdge = TaskBarPosition::Top;
  770. trayIconCenter = QPoint(screenRect.width(), 0);
  771. } else if(position == NotificationPosition::BottomLeft) {
  772. taskbarScreenEdge = TaskBarPosition::Bottom;
  773. trayIconCenter = QPoint(0, screenRect.height());
  774. } else if(position == NotificationPosition::BottomRight) {
  775. taskbarScreenEdge = TaskBarPosition::Bottom;
  776. trayIconCenter = QPoint(screenRect.width(), screenRect.height());
  777. }
  778. const auto topLeft = [=]() {
  779. switch(taskbarScreenEdge) {
  780. case TaskBarPosition::Bottom:
  781. return trayIconCenter.x() < screenRect.center().x() ? referencePoint - QPoint(0, height) : referencePoint - QPoint(width, height);
  782. case TaskBarPosition::Left:
  783. return trayIconCenter.y() < screenRect.center().y() ? referencePoint : referencePoint - QPoint(0, height);
  784. case TaskBarPosition::Top:
  785. return trayIconCenter.x() < screenRect.center().x() ? referencePoint : referencePoint - QPoint(width, 0);
  786. case TaskBarPosition::Right:
  787. return trayIconCenter.y() < screenRect.center().y() ? referencePoint - QPoint(width, 0) : QPoint(width, height);
  788. }
  789. Q_UNREACHABLE();
  790. }();
  791. const auto bottomRight = topLeft + QPoint(width, height);
  792. const auto windowRect = computeWindowRect(spacing, topLeft, bottomRight);
  793. qCDebug(lcSystray) << "taskbarScreenEdge:" << taskbarScreenEdge;
  794. qCDebug(lcSystray) << "screenRect:" << screenRect;
  795. qCDebug(lcSystray) << "windowRect (reference)" << QRect(topLeft, bottomRight);
  796. qCDebug(lcSystray) << "windowRect (adjusted)" << windowRect;
  797. qCDebug(lcSystray) << "referencePoint" << referencePoint;
  798. return windowRect.topLeft();
  799. }
  800. QPoint Systray::calcTrayIconCenter() const
  801. {
  802. if(geometry().isValid()) {
  803. // QSystemTrayIcon::geometry() is broken for ages on most Linux DEs (invalid geometry returned)
  804. // thus we can use this only for Windows and macOS
  805. auto trayIconCenter = geometry().center();
  806. return trayIconCenter;
  807. }
  808. // On Linux, fall back to mouse position (assuming tray icon is activated by mouse click)
  809. return QCursor::pos(currentScreen());
  810. }
  811. AccessManagerFactory::AccessManagerFactory()
  812. : QQmlNetworkAccessManagerFactory()
  813. {
  814. }
  815. QNetworkAccessManager* AccessManagerFactory::create(QObject *parent)
  816. {
  817. const auto am = new AccessManager(parent);
  818. const auto diskCache = new QNetworkDiskCache(am);
  819. diskCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
  820. am->setCache(diskCache);
  821. return am;
  822. }
  823. } // namespace OCC