454 lines
18 KiB
C++
454 lines
18 KiB
C++
/*
|
|
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
|
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
|
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* for more details.
|
|
*/
|
|
|
|
#include "application.h"
|
|
|
|
#include "account.h"
|
|
#include "accountmanager.h"
|
|
#include "accountstate.h"
|
|
#include "common/version.h"
|
|
#include "configfile.h"
|
|
#include "folder.h"
|
|
#include "folderman.h"
|
|
#include "gui/aboutdialog.h"
|
|
#include "gui/accountsettings.h"
|
|
#include "gui/fetchserversettings.h"
|
|
#include "gui/folderwizard/folderwizard.h"
|
|
#include "gui/newwizard/setupwizardcontroller.h"
|
|
#include "gui/notifications/systemnotification.h"
|
|
#include "gui/notifications/systemnotificationmanager.h"
|
|
#include "gui/systray.h"
|
|
#include "libsync/graphapi/spacesmanager.h"
|
|
#include "libsync/vfs/vfs.h"
|
|
#include "resources/fonticon.h"
|
|
#include "settingsdialog.h"
|
|
#include "socketapi/socketapi.h"
|
|
#include "theme.h"
|
|
|
|
#ifdef WITH_AUTO_UPDATER
|
|
#include "updater/ocupdater.h"
|
|
#endif
|
|
|
|
#if defined(Q_OS_WIN)
|
|
#include "gui/navigationpanehelper.h"
|
|
#include <qt_windows.h>
|
|
#endif
|
|
|
|
#include <QApplication>
|
|
#include <QCursor>
|
|
#include <QDesktopServices>
|
|
#include <QGuiApplication>
|
|
#include <QMenuBar>
|
|
#include <QScreen>
|
|
#include <QWindow>
|
|
|
|
using namespace Qt::Literals::StringLiterals;
|
|
using namespace OCC;
|
|
|
|
Q_LOGGING_CATEGORY(lcApplication, "gui.application", QtInfoMsg)
|
|
|
|
namespace {
|
|
|
|
void setUpInitialSyncFolder(AccountStatePtr accountStatePtr, bool useVfs)
|
|
{
|
|
// saves a bit of duplicate code
|
|
auto addFolder = [accountStatePtr, useVfs](const QString &localFolder, const QUrl &davUrl, const QString &spaceId = {}, const QString &displayName = {}) {
|
|
auto def = FolderDefinition{accountStatePtr->account()->uuid(), davUrl, spaceId, displayName};
|
|
def.setLocalPath(localFolder);
|
|
return FolderMan::instance()->addFolderFromWizard(accountStatePtr, std::move(def), useVfs);
|
|
};
|
|
|
|
auto finalize = [accountStatePtr] {
|
|
accountStatePtr->checkConnectivity();
|
|
FolderMan::instance()->setSyncEnabled(true);
|
|
FolderMan::instance()->scheduleAllFolders();
|
|
};
|
|
|
|
QObject::connect(
|
|
accountStatePtr->account()->spacesManager(), &GraphApi::SpacesManager::ready, accountStatePtr,
|
|
[accountStatePtr, addFolder, finalize] {
|
|
auto spaces = accountStatePtr->account()->spacesManager()->spaces();
|
|
// we do not want to set up folder sync connections for disabled spaces (#10173)
|
|
spaces.erase(std::remove_if(spaces.begin(), spaces.end(), [](auto *space) { return space->disabled(); }), spaces.end());
|
|
|
|
if (!spaces.isEmpty()) {
|
|
const QString localDir(accountStatePtr->account()->defaultSyncRoot());
|
|
FileSystem::setFolderMinimumPermissions(localDir);
|
|
Utility::setupFavLink(localDir);
|
|
for (const auto *space : spaces) {
|
|
const QString name = space->displayName();
|
|
const QString folderName = FolderMan::instance()->findGoodPathForNewSyncFolder(
|
|
localDir, name, FolderMan::NewFolderType::SpacesFolder, accountStatePtr->account()->uuid());
|
|
auto folder = addFolder(folderName, QUrl(space->drive().getRoot().getWebDavUrl()), space->drive().getRoot().getId(), name);
|
|
folder->setPriority(space->priority());
|
|
}
|
|
finalize();
|
|
}
|
|
},
|
|
Qt::SingleShotConnection);
|
|
accountStatePtr->account()->spacesManager()->checkReady();
|
|
}
|
|
|
|
void ensureWindowIntersectsAvailableScreen(QWidget *window)
|
|
{
|
|
if (!window) {
|
|
return;
|
|
}
|
|
|
|
const auto frame = window->frameGeometry();
|
|
for (const auto *screen : QGuiApplication::screens()) {
|
|
if (screen->availableGeometry().intersects(frame)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto *screen = QGuiApplication::screenAt(QCursor::pos());
|
|
if (!screen) {
|
|
screen = QGuiApplication::primaryScreen();
|
|
}
|
|
if (!screen) {
|
|
return;
|
|
}
|
|
|
|
const auto availableGeometry = screen->availableGeometry();
|
|
window->resize(window->size().boundedTo(availableGeometry.size()));
|
|
window->move(availableGeometry.center() - window->rect().center());
|
|
}
|
|
}
|
|
|
|
QString Application::displayLanguage() const
|
|
{
|
|
return _displayLanguage;
|
|
}
|
|
|
|
Application *Application::_instance = nullptr;
|
|
|
|
Application::Application(const QString &displayLanguage, bool debugMode)
|
|
: _debugMode(debugMode)
|
|
, _displayLanguage(displayLanguage)
|
|
, _updateNotifier(new UpdateNotifier(this))
|
|
{
|
|
// ensure the singleton works
|
|
{
|
|
_instance = this;
|
|
_settingsDialog = new SettingsDialog();
|
|
_systray = new Systray(this);
|
|
_systemNotificationManager = new SystemNotificationManager(this);
|
|
}
|
|
qCInfo(lcApplication) << u"Plugin search paths:" << qApp->libraryPaths();
|
|
|
|
// Check vfs plugins
|
|
if (VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::Mode::Off) {
|
|
qCWarning(lcApplication) << u"Theme wants to show vfs mode, but no vfs plugins are available";
|
|
}
|
|
qCInfo(lcApplication) << VfsPluginManager::instance().bestAvailableVfsMode() << u"plugin is available";
|
|
|
|
ConfigFile cfg;
|
|
|
|
// this should be called once during application startup to make sure we don't miss any messages
|
|
cfg.configureHttpLogging();
|
|
|
|
// The timeout is initialized with an environment variable, if not, override with the value from the config
|
|
if (AbstractNetworkJob::httpTimeout == AbstractNetworkJob::DefaultHttpTimeout) {
|
|
AbstractNetworkJob::httpTimeout = cfg.timeout();
|
|
}
|
|
|
|
qApp->setQuitOnLastWindowClosed(false);
|
|
|
|
connect(AccountManager::instance(), &AccountManager::accountAdded, this, &Application::slotAccountStateAdded);
|
|
for (const auto &ai : AccountManager::instance()->accounts()) {
|
|
slotAccountStateAdded(ai);
|
|
}
|
|
connect(_systemNotificationManager, &SystemNotificationManager::unknownNotificationClicked, this, &Application::showSettings);
|
|
connect(
|
|
_systemNotificationManager, &SystemNotificationManager::notificationFinished, this, [this](SystemNotification *, SystemNotification::Result result) {
|
|
if (result == SystemNotification::Result::Clicked) {
|
|
showSettings();
|
|
}
|
|
});
|
|
|
|
#ifdef WITH_AUTO_UPDATER
|
|
// Update checks
|
|
UpdaterScheduler *updaterScheduler = new UpdaterScheduler(this, this);
|
|
// the updater scheduler takes care of connecting its GUI bits to other components
|
|
(void)updaterScheduler;
|
|
#endif
|
|
|
|
// Cleanup at Quit.
|
|
connect(qApp, &QCoreApplication::aboutToQuit, this, &Application::slotCleanup);
|
|
|
|
#ifdef Q_OS_MAC
|
|
// add About to the global menu
|
|
QMenuBar *menuBar = new QMenuBar(nullptr);
|
|
// the menu name is not displayed
|
|
auto *menu = menuBar->addMenu(QString());
|
|
// the actual name is provided by mac
|
|
menu->addAction(QStringLiteral("About"), this, &Application::showAbout)->setMenuRole(QAction::AboutRole);
|
|
#endif
|
|
#ifdef Q_OS_WIN
|
|
// update the existing sidebar entries
|
|
NavigationPaneHelper::removeLegacyCloudStorageRegistry();
|
|
#endif
|
|
}
|
|
|
|
Application::~Application()
|
|
{
|
|
// Make sure all folders are gone, otherwise removing the
|
|
// accounts will remove the associated folders from the settings.
|
|
FolderMan::instance()->unloadAndDeleteAllFolders();
|
|
}
|
|
|
|
void Application::slotAccountStateAdded(AccountStatePtr accountState) const
|
|
{
|
|
connect(accountState->account().data(), &Account::serverVersionChanged, ocApp(), [account = accountState->account().data()] {
|
|
if (account->serverSupportLevel() != Account::ServerSupportLevel::Supported) {
|
|
ocApp()->systemNotificationManager()->notify({tr("Unsupported Server Version"),
|
|
tr("The server on account »%1« runs an unsupported version %2. "
|
|
"Using this client with unsupported server versions is untested and "
|
|
"potentially dangerous. Proceed at your own risk.")
|
|
.arg(account->displayNameWithHost(), account->capabilities().status().versionString()),
|
|
Resources::FontIcon(u'')});
|
|
}
|
|
});
|
|
|
|
// Hook up the folder manager slots to the account state's Q_SIGNALS:
|
|
connect(accountState.data(), &AccountState::isConnectedChanged, FolderMan::instance(), &FolderMan::slotIsConnectedChanged);
|
|
connect(accountState->account().data(), &Account::serverVersionChanged, FolderMan::instance(),
|
|
[account = accountState->account().data()] { FolderMan::instance()->slotServerVersionChanged(account); });
|
|
accountState->checkConnectivity();
|
|
}
|
|
|
|
void Application::slotCleanup()
|
|
{
|
|
ConfigFile().saveGeometry(_settingsDialog);
|
|
delete _settingsDialog;
|
|
|
|
// by now the credentials are supposed to be persisted
|
|
// don't start async credentials jobs during shutdown
|
|
AccountManager::instance()->save();
|
|
|
|
FolderMan::instance()->scheduler()->stop();
|
|
FolderMan::instance()->scheduler()->terminateCurrentSync(tr("Application is shutting down"));
|
|
FolderMan::instance()->unloadAndDeleteAllFolders();
|
|
|
|
// Remove the account from the account manager so it can be deleted.
|
|
AccountManager::instance()->shutdown();
|
|
}
|
|
|
|
AccountStatePtr Application::addNewAccount(AccountPtr newAccount)
|
|
{
|
|
auto *accountMan = AccountManager::instance();
|
|
|
|
// first things first: we need to add the new account
|
|
auto accountStatePtr = accountMan->addAccount(newAccount);
|
|
|
|
// if one account is configured: enable autostart
|
|
bool shouldSetAutoStart = (accountMan->accounts().size() == 1);
|
|
#ifdef Q_OS_MAC
|
|
// Don't auto start when not being 'installed'
|
|
shouldSetAutoStart = shouldSetAutoStart && QCoreApplication::applicationDirPath().startsWith(QLatin1String("/Applications/"));
|
|
#endif
|
|
if (shouldSetAutoStart) {
|
|
Utility::setLaunchOnStartup(Theme::instance()->appName(), Theme::instance()->appNameGUI(), true);
|
|
}
|
|
|
|
// showing the UI to show the user that the account has been added successfully
|
|
showSettings();
|
|
|
|
return accountStatePtr;
|
|
}
|
|
|
|
void Application::showSettings()
|
|
{
|
|
auto window = ocApp()->settingsDialog();
|
|
ensureWindowIntersectsAvailableScreen(window);
|
|
|
|
if (window->windowState().testFlag(Qt::WindowMinimized)) {
|
|
window->setWindowState(window->windowState() & ~Qt::WindowMinimized);
|
|
}
|
|
|
|
window->show();
|
|
ensureWindowIntersectsAvailableScreen(window);
|
|
window->raise();
|
|
window->activateWindow();
|
|
if (auto *windowHandle = window->windowHandle()) {
|
|
windowHandle->requestActivate();
|
|
}
|
|
|
|
#if defined(Q_OS_WIN)
|
|
// Windows disallows raising a Window when you're not the active application.
|
|
// Use a common hack to attach to the active application.
|
|
const auto foregroundWindow = GetForegroundWindow();
|
|
const auto foregroundThreadId = foregroundWindow ? GetWindowThreadProcessId(foregroundWindow, nullptr) : 0;
|
|
const auto currentThreadId = GetCurrentThreadId();
|
|
const auto attachedToForegroundThread =
|
|
foregroundThreadId != 0 && foregroundThreadId != currentThreadId && AttachThreadInput(currentThreadId, foregroundThreadId, true);
|
|
|
|
const auto hwnd = reinterpret_cast<HWND>(window->winId());
|
|
ShowWindow(hwnd, IsIconic(hwnd) ? SW_RESTORE : SW_SHOW);
|
|
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
|
|
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
|
|
SetForegroundWindow(hwnd);
|
|
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
|
|
|
|
if (attachedToForegroundThread) {
|
|
AttachThreadInput(currentThreadId, foregroundThreadId, false);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
SettingsDialog *Application::settingsDialog() const
|
|
{
|
|
return _settingsDialog;
|
|
}
|
|
|
|
void Application::showAbout()
|
|
{
|
|
if (!_aboutDialog) {
|
|
_aboutDialog = new AboutDialog(_settingsDialog);
|
|
_aboutDialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
_settingsDialog->addModalWidget(_aboutDialog);
|
|
}
|
|
}
|
|
|
|
SystemNotificationManager *Application::systemNotificationManager() const
|
|
{
|
|
return _systemNotificationManager;
|
|
}
|
|
|
|
void Application::runNewAccountWizard()
|
|
{
|
|
// passing the settings dialog as parent makes sure the wizard will be shown above it
|
|
// as the settingsDialog's lifetime spans across the entire application but the dialog will live much shorter,
|
|
// we have to clean it up manually when finished() is emitted
|
|
auto *wizardController = new Wizard::SetupWizardController(this->settingsDialog());
|
|
|
|
// while the wizard is shown, new syncs are disabled
|
|
FolderMan::instance()->setSyncEnabled(false);
|
|
|
|
connect(
|
|
wizardController, &Wizard::SetupWizardController::finished, this, [wizardController, this](const AccountPtr &newAccount, Wizard::SyncMode syncMode) {
|
|
// note: while the wizard is shown, we disable the folder synchronization
|
|
// previously we could perform this just here, but now we have to postpone this depending on whether selective sync was chosen
|
|
// see also #9497
|
|
|
|
// when the dialog is closed before it has finished, there won't be a new account to set up
|
|
// the wizard controller signalizes this by passing a null pointer
|
|
if (!newAccount.isNull()) {
|
|
// finally, call the slot that finalizes the setup
|
|
auto accountStatePtr = ocApp()->addNewAccount(newAccount);
|
|
accountStatePtr->setSettingUp(true);
|
|
|
|
_settingsDialog->setCurrentAccount(accountStatePtr->account().data());
|
|
|
|
// fetch server settings
|
|
auto fetchServerSettings = new FetchServerSettingsJob(accountStatePtr->account(), accountStatePtr->account().data());
|
|
|
|
connect(fetchServerSettings, &FetchServerSettingsJob::finishedSignal, accountStatePtr.data(), [accountStatePtr, syncMode, this] {
|
|
// saving once after adding makes sure the account is stored in the config in a working state
|
|
// this is needed to ensure a consistent state in the config file upon unexpected terminations of the client
|
|
// (for instance, when running from a debugger and stopping the process from there)
|
|
AccountManager::instance()->save();
|
|
|
|
// the account is now ready, emulate a normal account loading and Q_EMIT that the credentials are ready
|
|
Q_EMIT accountStatePtr->account()->credentialsFetched();
|
|
|
|
switch (syncMode) {
|
|
case Wizard::SyncMode::SyncEverything:
|
|
case Wizard::SyncMode::UseVfs: {
|
|
bool useVfs = syncMode == Wizard::SyncMode::UseVfs;
|
|
setUpInitialSyncFolder(accountStatePtr, useVfs);
|
|
accountStatePtr->setSettingUp(false);
|
|
break;
|
|
}
|
|
case Wizard::SyncMode::ConfigureUsingFolderWizard: {
|
|
Q_ASSERT(!accountStatePtr->account()->hasDefaultSyncRoot());
|
|
|
|
auto *folderWizard = new FolderWizard(accountStatePtr, ocApp()->settingsDialog());
|
|
folderWizard->setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
// TODO: duplication of AccountSettings
|
|
// adapted from AccountSettings::slotFolderWizardAccepted()
|
|
connect(folderWizard, &QDialog::accepted, accountStatePtr.data(), [accountStatePtr, folderWizard]() {
|
|
FolderMan *folderMan = FolderMan::instance();
|
|
|
|
qCInfo(lcApplication) << u"Folder wizard completed";
|
|
const auto config = folderWizard->result();
|
|
|
|
auto folder = folderMan->addFolderFromFolderWizardResult(accountStatePtr, config);
|
|
|
|
if (!config.selectiveSyncBlackList.isEmpty() && OC_ENSURE(folder && !config.useVirtualFiles)) {
|
|
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, config.selectiveSyncBlackList);
|
|
|
|
// The user already accepted the selective sync dialog. everything is in the white list
|
|
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, {QLatin1String("/")});
|
|
}
|
|
|
|
folderMan->setSyncEnabled(true);
|
|
folderMan->scheduleAllFolders();
|
|
accountStatePtr->setSettingUp(false);
|
|
});
|
|
|
|
connect(folderWizard, &QDialog::rejected, accountStatePtr.data(), [accountStatePtr]() {
|
|
qCInfo(lcApplication) << u"Folder wizard cancelled";
|
|
FolderMan::instance()->setSyncEnabled(true);
|
|
accountStatePtr->setSettingUp(false);
|
|
});
|
|
|
|
_settingsDialog->accountSettings(accountStatePtr->account().get())
|
|
->addModalLegacyDialog(folderWizard, AccountSettings::ModalWidgetSizePolicy::Expanding);
|
|
break;
|
|
}
|
|
case OCC::Wizard::SyncMode::Invalid:
|
|
Q_UNREACHABLE();
|
|
}
|
|
});
|
|
fetchServerSettings->start();
|
|
} else {
|
|
FolderMan::instance()->setSyncEnabled(true);
|
|
}
|
|
|
|
// make sure the wizard is cleaned up eventually
|
|
wizardController->deleteLater();
|
|
});
|
|
|
|
// all we have to do is show the dialog...
|
|
settingsDialog()->addModalWidget(wizardController->window());
|
|
}
|
|
|
|
QSystemTrayIcon *Application::systemTrayIcon() const
|
|
{
|
|
return _systray;
|
|
}
|
|
|
|
UpdateNotifier *Application::updateNotifier() const
|
|
{
|
|
return _updateNotifier;
|
|
}
|
|
|
|
bool Application::debugMode()
|
|
{
|
|
return _debugMode;
|
|
}
|
|
|
|
std::unique_ptr<Application> Application::createInstance(const QString &displayLanguage, bool debugMode)
|
|
{
|
|
Q_ASSERT(!_instance);
|
|
// _instance will be set in the constructor
|
|
new Application(displayLanguage, debugMode);
|
|
return std::unique_ptr<Application>(_instance);
|
|
}
|