nextcloud-desktop/src/gui/owncloudgui.cpp

684 lines
22 KiB
C++
Raw Normal View History

/*
* Copyright (C) by Klaas Freitag <freitag@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 "owncloudgui.h"
#include "theme.h"
#include "folderman.h"
#include "progressdispatcher.h"
#include "owncloudsetupwizard.h"
#include "sharedialog.h"
2017-05-17 11:55:42 +03:00
#include "settingsdialog.h"
#include "logger.h"
#include "logbrowser.h"
#include "account.h"
#include "accountstate.h"
#include "openfilemanager.h"
#include "accountmanager.h"
2017-09-01 19:11:43 +03:00
#include "common/syncjournalfilerecord.h"
#include "creds/abstractcredentials.h"
#ifdef WITH_LIBCLOUDPROVIDERS
#include "cloudproviders/cloudprovidermanager.h"
#endif
#include <QQmlApplicationEngine>
#include <QDesktopServices>
2016-03-30 13:19:09 +03:00
#include <QDir>
#include <QMessageBox>
#include <QSignalMapper>
#ifdef WITH_LIBCLOUDPROVIDERS
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusInterface>
#endif
#if defined(Q_OS_X11)
#include <QX11Info>
#endif
#include <QQmlEngine>
#include <QQmlComponent>
#include <QQmlApplicationEngine>
#include <QQuickItem>
#include <QQmlContext>
2014-11-10 00:34:07 +03:00
namespace OCC {
const char propertyAccountC[] = "oc_account";
2017-05-17 11:55:42 +03:00
ownCloudGui::ownCloudGui(Application *parent)
: QObject(parent)
, _tray(nullptr)
, _settingsDialog(nullptr)
, _logBrowser(nullptr)
#ifdef WITH_LIBCLOUDPROVIDERS
, _bus(QDBusConnection::sessionBus())
#endif
2017-05-17 11:55:42 +03:00
, _app(parent)
{
_tray = Systray::instance();
_tray->setTrayEngine(new QQmlApplicationEngine(this));
// for the beginning, set the offline icon until the account was verified
_tray->setIcon(Theme::instance()->folderOfflineIcon(/*systray?*/ true));
_tray->show();
connect(_tray.data(), &QSystemTrayIcon::activated,
this, &ownCloudGui::slotTrayClicked);
connect(_tray.data(), &Systray::openHelp,
this, &ownCloudGui::slotHelp);
connect(_tray.data(), &Systray::openAccountWizard,
this, &ownCloudGui::slotNewAccountWizard);
connect(_tray.data(), &Systray::openMainDialog,
this, &ownCloudGui::slotOpenMainDialog);
connect(_tray.data(), &Systray::openSettings,
this, &ownCloudGui::slotShowSettings);
connect(_tray.data(), &Systray::shutdown,
this, &ownCloudGui::slotShutdown);
connect(_tray.data(), &Systray::openShareDialog,
this, [=](const QString &sharePath, const QString &localPath) {
slotShowShareDialog(sharePath, localPath, ShareDialogStartPage::UsersAndGroups);
});
ProgressDispatcher *pd = ProgressDispatcher::instance();
connect(pd, &ProgressDispatcher::progressInfo, this,
&ownCloudGui::slotUpdateProgress);
FolderMan *folderMan = FolderMan::instance();
connect(folderMan, &FolderMan::folderSyncStateChange,
this, &ownCloudGui::slotSyncStateChange);
connect(Logger::instance(), &Logger::guiLog,
this, &ownCloudGui::slotShowTrayMessage);
connect(Logger::instance(), &Logger::optionalGuiLog,
this, &ownCloudGui::slotShowOptionalTrayMessage);
connect(Logger::instance(), &Logger::guiMessage,
this, &ownCloudGui::slotShowGuiMessage);
}
void ownCloudGui::createTray()
{
_tray->create();
}
#ifdef WITH_LIBCLOUDPROVIDERS
void ownCloudGui::setupCloudProviders()
{
new CloudProviderManager(this);
}
bool ownCloudGui::cloudProviderApiAvailable()
{
if (!_bus.isConnected()) {
return false;
}
QDBusInterface dbus_iface("org.freedesktop.CloudProviderManager", "/org/freedesktop/CloudProviderManager",
"org.freedesktop.CloudProvider.Manager1", _bus);
if (!dbus_iface.isValid()) {
qCInfo(lcApplication) << "DBus interface unavailable";
return false;
}
return true;
}
#endif
2014-11-10 00:30:29 +03:00
// This should rather be in application.... or rather in ConfigFile?
void ownCloudGui::slotOpenSettingsDialog()
{
// if account is set up, start the configuration wizard.
2017-05-17 11:55:42 +03:00
if (!AccountManager::instance()->accounts().isEmpty()) {
if (_settingsDialog.isNull() || QApplication::activeWindow() != _settingsDialog) {
slotShowSettings();
} else {
_settingsDialog->close();
}
} else {
qCInfo(lcApplication) << "No configured folders yet, starting setup wizard";
slotNewAccountWizard();
}
}
void ownCloudGui::slotOpenMainDialog()
{
if (!_tray->isOpen()) {
_tray->showWindow();
}
}
2017-05-17 11:55:42 +03:00
void ownCloudGui::slotTrayClicked(QSystemTrayIcon::ActivationReason reason)
{
if (reason == QSystemTrayIcon::Trigger) {
if (OwncloudSetupWizard::bringWizardToFrontIfVisible()) {
// brought wizard to front
} else if (_shareDialogs.size() > 0) {
// Share dialog(s) be hidden by other apps, bring them back
2017-05-17 11:55:42 +03:00
Q_FOREACH (const QPointer<ShareDialog> &shareDialog, _shareDialogs) {
Q_ASSERT(shareDialog.data());
raiseDialog(shareDialog);
}
} else if (_tray->isOpen()) {
_tray->hideWindow();
} else {
if (AccountManager::instance()->accounts().isEmpty()) {
this->slotOpenSettingsDialog();
} else {
_tray->showWindow();
}
}
}
// FIXME: Also make sure that any auto updater dialogue https://github.com/owncloud/client/issues/5613
// or SSL error dialog also comes to front.
}
2017-05-17 11:55:42 +03:00
void ownCloudGui::slotSyncStateChange(Folder *folder)
{
slotComputeOverallSyncStatus();
2017-05-17 11:55:42 +03:00
if (!folder) {
2014-08-19 16:08:31 +04:00
return; // Valid, just a general GUI redraw was needed.
}
auto result = folder->syncResult();
2017-05-17 11:55:42 +03:00
qCInfo(lcApplication) << "Sync state changed for folder " << folder->remoteUrl().toString() << ": " << result.statusString();
if (result.status() == SyncResult::Success
|| result.status() == SyncResult::Problem
|| result.status() == SyncResult::SyncAbortRequested
|| result.status() == SyncResult::Error) {
Logger::instance()->enterNextLogFile();
}
}
void ownCloudGui::slotFoldersChanged()
{
slotComputeOverallSyncStatus();
}
void ownCloudGui::slotOpenPath(const QString &path)
{
showInFileManager(path);
}
void ownCloudGui::slotAccountStateChanged()
{
slotComputeOverallSyncStatus();
}
2017-05-17 11:55:42 +03:00
void ownCloudGui::slotTrayMessageIfServerUnsupported(Account *account)
{
if (account->serverVersionUnsupported()) {
slotShowTrayMessage(
2017-05-17 11:55:42 +03:00
tr("Unsupported Server Version"),
tr("The server on account %1 runs an old and unsupported version %2. "
"Using this client with unsupported server versions is untested and "
"potentially dangerous. Proceed at your own risk.")
.arg(account->displayName(), account->serverVersion()));
}
}
void ownCloudGui::slotComputeOverallSyncStatus()
{
bool allSignedOut = true;
2016-03-01 18:07:11 +03:00
bool allPaused = true;
bool allDisconnected = true;
QVector<AccountStatePtr> problemAccounts;
auto setStatusText = [&](const QString &text) {
// FIXME: So this doesn't do anything? Needs to be revisited
Q_UNUSED(text)
// Don't overwrite the status if we're currently syncing
if (FolderMan::instance()->currentSyncFolder())
return;
//_actionStatus->setText(text);
};
foreach (auto a, AccountManager::instance()->accounts()) {
if (!a->isSignedOut()) {
allSignedOut = false;
}
if (!a->isConnected()) {
problemAccounts.append(a);
} else {
allDisconnected = false;
}
}
2017-05-17 11:55:42 +03:00
foreach (Folder *f, FolderMan::instance()->map()) {
2016-03-01 18:07:11 +03:00
if (!f->syncPaused()) {
allPaused = false;
}
}
if (!problemAccounts.empty()) {
_tray->setIcon(Theme::instance()->folderOfflineIcon(true));
if (allDisconnected) {
setStatusText(tr("Disconnected"));
} else {
setStatusText(tr("Disconnected from some accounts"));
}
#ifdef Q_OS_WIN
// Windows has a 128-char tray tooltip length limit.
QStringList accountNames;
foreach (AccountStatePtr a, problemAccounts) {
accountNames.append(a->account()->displayName());
}
2017-05-17 11:55:42 +03:00
_tray->setToolTip(tr("Disconnected from %1").arg(accountNames.join(QLatin1String(", "))));
#else
QStringList messages;
messages.append(tr("Disconnected from accounts:"));
foreach (AccountStatePtr a, problemAccounts) {
2017-05-17 11:55:42 +03:00
QString message = tr("Account %1: %2").arg(a->account()->displayName(), a->stateString(a->state()));
if (!a->connectionErrors().empty()) {
message += QLatin1String("\n");
message += a->connectionErrors().join(QLatin1String("\n"));
}
messages.append(message);
}
_tray->setToolTip(messages.join(QLatin1String("\n\n")));
#endif
return;
}
if (allSignedOut) {
_tray->setIcon(Theme::instance()->folderOfflineIcon(true));
_tray->setToolTip(tr("Please sign in"));
setStatusText(tr("Signed out"));
return;
2016-03-01 18:07:11 +03:00
} else if (allPaused) {
_tray->setIcon(Theme::instance()->syncStateIcon(SyncResult::Paused, true));
2016-03-01 18:07:11 +03:00
_tray->setToolTip(tr("Account synchronization is disabled"));
setStatusText(tr("Synchronization is paused"));
2016-03-01 18:07:11 +03:00
return;
}
// display the info of the least successful sync (eg. do not just display the result of the latest sync)
QString trayMessage;
FolderMan *folderMan = FolderMan::instance();
Folder::Map map = folderMan->map();
SyncResult::Status overallStatus = SyncResult::Undefined;
bool hasUnresolvedConflicts = false;
FolderMan::trayOverallStatus(map.values(), &overallStatus, &hasUnresolvedConflicts);
// If the sync succeeded but there are unresolved conflicts,
// show the problem icon!
auto iconStatus = overallStatus;
if (iconStatus == SyncResult::Success && hasUnresolvedConflicts) {
iconStatus = SyncResult::Problem;
}
// If we don't get a status for whatever reason, that's a Problem
if (iconStatus == SyncResult::Undefined) {
iconStatus = SyncResult::Problem;
}
QIcon statusIcon = Theme::instance()->syncStateIcon(iconStatus, true);
_tray->setIcon(statusIcon);
// create the tray blob message, check if we have an defined state
if (map.count() > 0) {
#ifdef Q_OS_WIN
// Windows has a 128-char tray tooltip length limit.
trayMessage = folderMan->trayTooltipStatusString(overallStatus, hasUnresolvedConflicts, false);
#else
QStringList allStatusStrings;
foreach (Folder *folder, map.values()) {
QString folderMessage = FolderMan::trayTooltipStatusString(
folder->syncResult().status(),
folder->syncResult().hasUnresolvedConflicts(),
folder->syncPaused());
allStatusStrings += tr("Folder %1: %2").arg(folder->shortGuiLocalPath(), folderMessage);
}
trayMessage = allStatusStrings.join(QLatin1String("\n"));
#endif
_tray->setToolTip(trayMessage);
if (overallStatus == SyncResult::Success || overallStatus == SyncResult::Problem) {
if (hasUnresolvedConflicts) {
setStatusText(tr("Unresolved conflicts"));
} else {
setStatusText(tr("Up to date"));
}
} else if (overallStatus == SyncResult::Paused) {
setStatusText(tr("Synchronization is paused"));
} else {
setStatusText(tr("Error during synchronization"));
}
} else {
_tray->setToolTip(tr("There are no sync folders configured."));
setStatusText(tr("No sync folders configured"));
}
}
void ownCloudGui::slotShowTrayMessage(const QString &title, const QString &msg)
{
2017-05-17 11:55:42 +03:00
if (_tray)
_tray->showMessage(title, msg);
else
qCWarning(lcApplication) << "Tray not ready: " << msg;
}
void ownCloudGui::slotShowOptionalTrayMessage(const QString &title, const QString &msg)
{
slotShowTrayMessage(title, msg);
}
/*
* open the folder with the given Alias
*/
2017-05-17 11:55:42 +03:00
void ownCloudGui::slotFolderOpenAction(const QString &alias)
{
Folder *f = FolderMan::instance()->folder(alias);
2017-05-17 11:55:42 +03:00
if (f) {
qCInfo(lcApplication) << "opening local url " << f->path();
2014-06-20 13:26:41 +04:00
QUrl url = QUrl::fromLocalFile(f->path());
#ifdef Q_OS_WIN
// work around a bug in QDesktopServices on Win32, see i-net
QString filePath = f->path();
if (filePath.startsWith(QLatin1String("\\\\")) || filePath.startsWith(QLatin1String("//")))
2015-01-14 19:39:14 +03:00
url = QUrl::fromLocalFile(QDir::toNativeSeparators(filePath));
else
url = QUrl::fromLocalFile(filePath);
#endif
QDesktopServices::openUrl(url);
}
}
2017-05-17 11:55:42 +03:00
void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo &progress)
{
Q_UNUSED(folder);
// FIXME: Lots of messages computed for nothing in this method, needs revisiting
if (progress.status() == ProgressInfo::Discovery) {
#if 0
if (!progress._currentDiscoveredRemoteFolder.isEmpty()) {
_actionStatus->setText(tr("Checking for changes in remote '%1'")
.arg(progress._currentDiscoveredRemoteFolder));
} else if (!progress._currentDiscoveredLocalFolder.isEmpty()) {
_actionStatus->setText(tr("Checking for changes in local '%1'")
.arg(progress._currentDiscoveredLocalFolder));
}
#endif
} else if (progress.status() == ProgressInfo::Done) {
QTimer::singleShot(2000, this, &ownCloudGui::slotComputeOverallSyncStatus);
}
if (progress.status() != ProgressInfo::Propagation) {
return;
}
if (progress.totalSize() == 0) {
quint64 currentFile = progress.currentFile();
quint64 totalFileCount = qMax(progress.totalFiles(), currentFile);
QString msg;
if (progress.trustEta()) {
msg = tr("Syncing %1 of %2 (%3 left)")
2017-05-17 11:55:42 +03:00
.arg(currentFile)
.arg(totalFileCount)
.arg(Utility::durationToDescriptiveString2(progress.totalProgress().estimatedEta));
} else {
msg = tr("Syncing %1 of %2")
2017-05-17 11:55:42 +03:00
.arg(currentFile)
.arg(totalFileCount);
}
//_actionStatus->setText(msg);
} else {
2017-05-17 11:55:42 +03:00
QString totalSizeStr = Utility::octetsToString(progress.totalSize());
QString msg;
if (progress.trustEta()) {
msg = tr("Syncing %1 (%2 left)")
2017-05-17 11:55:42 +03:00
.arg(totalSizeStr, Utility::durationToDescriptiveString2(progress.totalProgress().estimatedEta));
} else {
msg = tr("Syncing %1")
2017-05-17 11:55:42 +03:00
.arg(totalSizeStr);
}
//_actionStatus->setText(msg);
}
if (!progress._lastCompletedItem.isEmpty()) {
QString kindStr = Progress::asResultString(progress._lastCompletedItem);
QString timeStr = QTime::currentTime().toString("hh:mm");
QString actionText = tr("%1 (%2, %3)").arg(progress._lastCompletedItem._file, kindStr, timeStr);
auto *action = new QAction(actionText, this);
Folder *f = FolderMan::instance()->folder(folder);
if (f) {
QString fullPath = f->path() + '/' + progress._lastCompletedItem._file;
if (QFile(fullPath).exists()) {
connect(action, &QAction::triggered, this, [this, fullPath] { this->slotOpenPath(fullPath); });
} else {
action->setEnabled(false);
}
}
if (_recentItemsActions.length() > 5) {
_recentItemsActions.takeFirst()->deleteLater();
}
_recentItemsActions.append(action);
}
}
void ownCloudGui::slotLogin()
{
if (auto account = qvariant_cast<AccountStatePtr>(sender()->property(propertyAccountC))) {
account->account()->resetRejectedCertificates();
account->signIn();
} else {
2016-03-01 18:07:11 +03:00
auto list = AccountManager::instance()->accounts();
foreach (const auto &a, list) {
a->signIn();
}
}
}
void ownCloudGui::slotLogout()
{
auto list = AccountManager::instance()->accounts();
if (auto account = qvariant_cast<AccountStatePtr>(sender()->property(propertyAccountC))) {
list.clear();
list.append(account);
}
foreach (const auto &ai, list) {
ai->signOutByUi();
}
}
void ownCloudGui::slotNewAccountWizard()
{
OwncloudSetupWizard::runWizard(qApp, SLOT(slotownCloudWizardDone(int)));
}
void ownCloudGui::slotShowGuiMessage(const QString &title, const QString &message)
{
auto *msgBox = new QMessageBox;
msgBox->setWindowFlags(msgBox->windowFlags() | Qt::WindowStaysOnTopHint);
msgBox->setAttribute(Qt::WA_DeleteOnClose);
msgBox->setText(message);
msgBox->setWindowTitle(title);
msgBox->setIcon(QMessageBox::Information);
msgBox->open();
}
void ownCloudGui::slotShowSettings()
{
if (_settingsDialog.isNull()) {
_settingsDialog = new SettingsDialog(this);
2017-05-17 11:55:42 +03:00
_settingsDialog->setAttribute(Qt::WA_DeleteOnClose, true);
_settingsDialog->show();
}
raiseDialog(_settingsDialog.data());
}
void ownCloudGui::slotSettingsDialogActivated()
{
emit isShowingSettingsDialog();
}
void ownCloudGui::slotShowSyncProtocol()
{
slotShowSettings();
//_settingsDialog->showActivityPage();
}
void ownCloudGui::slotShutdown()
{
// explicitly close windows. This is somewhat of a hack to ensure
// that saving the geometries happens ASAP during a OS shutdown
// those do delete on close
2017-05-17 11:55:42 +03:00
if (!_settingsDialog.isNull())
_settingsDialog->close();
if (!_logBrowser.isNull())
_logBrowser->deleteLater();
_app->quit();
}
void ownCloudGui::slotToggleLogBrowser()
{
if (_logBrowser.isNull()) {
// init the log browser.
_logBrowser = new LogBrowser;
// ## TODO: allow new log name maybe?
}
2017-05-17 11:55:42 +03:00
if (_logBrowser->isVisible()) {
_logBrowser->hide();
} else {
raiseDialog(_logBrowser);
}
}
void ownCloudGui::slotOpenOwnCloud()
{
if (auto account = qvariant_cast<AccountPtr>(sender()->property(propertyAccountC))) {
QDesktopServices::openUrl(account->url());
}
}
void ownCloudGui::slotHelp()
{
QDesktopServices::openUrl(QUrl(Theme::instance()->helpUrl()));
}
2017-05-17 11:55:42 +03:00
void ownCloudGui::raiseDialog(QWidget *raiseWidget)
{
if (raiseWidget && !raiseWidget->parentWidget()) {
// Qt has a bug which causes parent-less dialogs to pop-under.
raiseWidget->showNormal();
raiseWidget->raise();
raiseWidget->activateWindow();
#if defined(Q_OS_X11)
WId wid = widget->winId();
NETWM::init();
XEvent e;
e.xclient.type = ClientMessage;
e.xclient.message_type = NETWM::NET_ACTIVE_WINDOW;
e.xclient.display = QX11Info::display();
e.xclient.window = wid;
e.xclient.format = 32;
e.xclient.data.l[0] = 2;
e.xclient.data.l[1] = QX11Info::appTime();
e.xclient.data.l[2] = 0;
e.xclient.data.l[3] = 0l;
e.xclient.data.l[4] = 0l;
Display *display = QX11Info::display();
XSendEvent(display,
2017-05-17 11:55:42 +03:00
RootWindow(display, DefaultScreen(display)),
False, // propagate
SubstructureRedirectMask | SubstructureNotifyMask,
&e);
#endif
}
}
void ownCloudGui::slotShowShareDialog(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage)
{
const auto folder = FolderMan::instance()->folderForPath(localPath);
if (!folder) {
qCWarning(lcApplication) << "Could not open share dialog for" << localPath << "no responsible folder found";
return;
}
// For https://github.com/owncloud/client/issues/3783
if (_settingsDialog) {
_settingsDialog->hide();
}
const auto accountState = folder->accountState();
const QString file = localPath.mid(folder->cleanPath().length() + 1);
SyncJournalFileRecord fileRecord;
bool resharingAllowed = true; // lets assume the good
if (folder->journalDb()->getFileRecord(file, &fileRecord) && fileRecord.isValid()) {
// check the permission: Is resharing allowed?
if (!fileRecord._remotePerm.isNull() && !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
resharingAllowed = false;
}
}
// As a first approximation, set the set of permissions that can be granted
// either to everything (resharing allowed) or nothing (no resharing).
//
// The correct value will be found with a propfind from ShareDialog.
// (we want to show the dialog directly, not wait for the propfind first)
SharePermissions maxSharingPermissions =
2017-05-17 11:55:42 +03:00
SharePermissionRead
| SharePermissionUpdate | SharePermissionCreate | SharePermissionDelete
| SharePermissionShare;
if (!resharingAllowed) {
maxSharingPermissions = {};
}
ShareDialog *w = nullptr;
if (_shareDialogs.contains(localPath) && _shareDialogs[localPath]) {
qCInfo(lcApplication) << "Raising share dialog" << sharePath << localPath;
w = _shareDialogs[localPath];
} else {
qCInfo(lcApplication) << "Opening share dialog" << sharePath << localPath << maxSharingPermissions;
w = new ShareDialog(accountState, sharePath, localPath, maxSharingPermissions, fileRecord.numericFileId(), startPage);
2017-05-17 11:55:42 +03:00
w->setAttribute(Qt::WA_DeleteOnClose, true);
_shareDialogs[localPath] = w;
connect(w, &QObject::destroyed, this, &ownCloudGui::slotRemoveDestroyedShareDialogs);
}
raiseDialog(w);
}
void ownCloudGui::slotRemoveDestroyedShareDialogs()
{
2017-05-17 11:55:42 +03:00
QMutableMapIterator<QString, QPointer<ShareDialog>> it(_shareDialogs);
while (it.hasNext()) {
it.next();
2017-05-17 11:55:42 +03:00
if (!it.value() || it.value() == sender()) {
it.remove();
}
}
}
} // end namespace