diff --git a/client.qrc b/client.qrc index f9952965a480..bb284644a91f 100644 --- a/client.qrc +++ b/client.qrc @@ -2,6 +2,7 @@ resources/settings.png resources/settings@2x.png + resources/activity.svg resources/activity.png resources/activity@2x.png resources/network.png @@ -11,6 +12,7 @@ resources/lock-https.png resources/lock-https@2x.png resources/account.png + resources/account.svg resources/more.svg resources/delete.png resources/close.svg @@ -28,7 +30,14 @@ resources/copy.svg resources/state-sync.svg resources/add.png + resources/add-color.svg resources/state-info.svg + resources/change.svg + resources/delete-color.svg + + src/gui/tray/Window.qml + src/gui/tray/UserLine.qml + diff --git a/resources/add-color.svg b/resources/add-color.svg new file mode 100644 index 000000000000..cb4596ac1e56 --- /dev/null +++ b/resources/add-color.svg @@ -0,0 +1 @@ + diff --git a/resources/change.svg b/resources/change.svg new file mode 100644 index 000000000000..12071422b7f9 --- /dev/null +++ b/resources/change.svg @@ -0,0 +1 @@ + diff --git a/resources/delete-color.svg b/resources/delete-color.svg new file mode 100644 index 000000000000..72aed6b937da --- /dev/null +++ b/resources/delete-color.svg @@ -0,0 +1 @@ + diff --git a/src/common/utility.cpp b/src/common/utility.cpp index c0a22177f0cb..8229cab0579e 100644 --- a/src/common/utility.cpp +++ b/src/common/utility.cpp @@ -255,7 +255,7 @@ void Utility::usleep(int usec) } // This can be overriden from the tests -OCSYNC_EXPORT bool fsCasePreserving_override = []()-> bool { +OCSYNC_EXPORT bool fsCasePreserving_override = []() -> bool { QByteArray env = qgetenv("OWNCLOUD_TEST_CASE_PRESERVING"); if (!env.isEmpty()) return env.toInt(); @@ -362,12 +362,12 @@ QString Utility::fileNameForGuiUse(const QString &fName) QByteArray Utility::normalizeEtag(QByteArray etag) { /* strip "XXXX-gzip" */ - if(etag.startsWith('"') && etag.endsWith("-gzip\"")) { + if (etag.startsWith('"') && etag.endsWith("-gzip\"")) { etag.chop(6); etag.remove(0, 1); } /* strip trailing -gzip */ - if(etag.endsWith("-gzip")) { + if (etag.endsWith("-gzip")) { etag.chop(5); } /* strip normal quotes */ @@ -400,7 +400,7 @@ void Utility::crash() // without compiler warnings about possible truncation uint Utility::convertSizeToUint(size_t &convertVar) { - if( convertVar > UINT_MAX ) { + if (convertVar > UINT_MAX) { //throw std::bad_cast(); convertVar = UINT_MAX; // intentionally default to wrong value here to not crash: exception handling TBD } @@ -409,7 +409,7 @@ uint Utility::convertSizeToUint(size_t &convertVar) uint Utility::convertSizeToInt(size_t &convertVar) { - if( convertVar > INT_MAX ) { + if (convertVar > INT_MAX) { //throw std::bad_cast(); convertVar = INT_MAX; // intentionally default to wrong value here to not crash: exception handling TBD } @@ -465,7 +465,7 @@ QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from) if (floor(secs / 3600.0) > 0) { int hours = floor(secs / 3600.0); - if(hours == 1){ + if (hours == 1) { return (QObject::tr("%n hour ago", "", hours)); } else { return (QObject::tr("%n hours ago", "", hours)); @@ -480,7 +480,7 @@ QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from) return QObject::tr("Less than a minute ago"); } - } else if(minutes == 1){ + } else if (minutes == 1) { return (QObject::tr("%n minute ago", "", minutes)); } else { return (QObject::tr("%n minutes ago", "", minutes)); diff --git a/src/common/utility.h b/src/common/utility.h index 803f690c218d..e8a2435466ce 100644 --- a/src/common/utility.h +++ b/src/common/utility.h @@ -20,6 +20,7 @@ #ifndef UTILITY_H #define UTILITY_H + #include "ocsynclib.h" #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index dbfc10214969..4ccf3abaaee9 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1,5 +1,5 @@ project(gui) -find_package(Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt5 REQUIRED COMPONENTS Widgets Svg) set(CMAKE_AUTOMOC TRUE) set(CMAKE_AUTOUIC TRUE) set(CMAKE_AUTORCC TRUE) @@ -24,7 +24,6 @@ set(client_UI_SRCS ignorelisteditor.ui ignorelisttablewidget.ui networksettings.ui - activitywidget.ui synclogdialog.ui settingsdialog.ui sharedialog.ui @@ -35,6 +34,8 @@ set(client_UI_SRCS addcertificatedialog.ui proxyauthdialog.ui mnemonicdialog.ui + tray/Window.qml + tray/UserLine.qml wizard/flow2authwidget.ui wizard/owncloudadvancedsetuppage.ui wizard/owncloudconnectionmethoddialog.ui @@ -73,10 +74,6 @@ set(client_SRCS openfilemanager.cpp owncloudgui.cpp owncloudsetupwizard.cpp - activitydata.cpp - activitylistmodel.cpp - activitywidget.cpp - activityitemdelegate.cpp selectivesyncdialog.cpp settingsdialog.cpp sharedialog.cpp @@ -99,12 +96,15 @@ set(client_SRCS synclogdialog.cpp tooltipupdater.cpp notificationconfirmjob.cpp - servernotificationhandler.cpp guiutility.cpp elidedlabel.cpp headerbanner.cpp iconjob.cpp remotewipe.cpp + tray/ActivityData.cpp + tray/ActivityListModel.cpp + tray/UserModel.cpp + tray/NotificationHandler.cpp creds/credentialsfactory.cpp creds/httpcredentialsgui.cpp creds/oauth.cpp @@ -298,7 +298,7 @@ else() endif() add_library(updater STATIC ${updater_SRCS}) -target_link_libraries(updater ${synclib_NAME} Qt5::Widgets Qt5::Network Qt5::Xml Qt5::WebEngineWidgets) +target_link_libraries(updater ${synclib_NAME} Qt5::Widgets Qt5::Svg Qt5::Network Qt5::Xml Qt5::WebEngineWidgets) target_include_directories(updater PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) set_target_properties( ${APPLICATION_EXECUTABLE} PROPERTIES @@ -308,7 +308,7 @@ set_target_properties( ${APPLICATION_EXECUTABLE} PROPERTIES set_target_properties( ${APPLICATION_EXECUTABLE} PROPERTIES INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}/${APPLICATION_EXECUTABLE};${CMAKE_INSTALL_RPATH}" ) -target_link_libraries( ${APPLICATION_EXECUTABLE} Qt5::Widgets Qt5::Network Qt5::Xml) +target_link_libraries( ${APPLICATION_EXECUTABLE} Qt5::Widgets Qt5::Svg Qt5::Network Qt5::Xml) target_link_libraries( ${APPLICATION_EXECUTABLE} ${synclib_NAME} ) target_link_libraries( ${APPLICATION_EXECUTABLE} updater ) target_link_libraries( ${APPLICATION_EXECUTABLE} ${OS_SPECIFIC_LINK_LIBRARIES} ) diff --git a/src/gui/accountmanager.h b/src/gui/accountmanager.h index fabc6ca5b620..4719b8109d07 100644 --- a/src/gui/accountmanager.h +++ b/src/gui/accountmanager.h @@ -89,6 +89,9 @@ class AccountManager : public QObject // Adds an account to the tracked list, emitting accountAdded() void addAccountState(AccountState *accountState); + AccountManager() {} + QList _accounts; + public slots: /// Saves account data, not including the credentials void saveAccount(Account *a); @@ -104,9 +107,5 @@ public slots: void accountAdded(AccountState *account); void accountRemoved(AccountState *account); void removeAccountFolders(AccountState *account); - -private: - AccountManager() {} - QList _accounts; }; } diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index e79ff3463c76..1c9e3b47d6ca 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -143,9 +143,6 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) _ui->_folderList->setAttribute(Qt::WA_Hover, true); _ui->_folderList->installEventFilter(mouseCursorChanger); - createAccountToolbox(); - connect(AccountManager::instance(), &AccountManager::accountAdded, - this, &AccountSettings::slotAccountAdded); connect(this, &AccountSettings::removeAccountFolders, AccountManager::instance(), &AccountManager::removeAccountFolders); connect(_ui->_folderList, &QWidget::customContextMenuRequested, @@ -207,33 +204,10 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) _ui->encryptionMessage->hide(); } - customizeStyle(); -} - - -void AccountSettings::createAccountToolbox() -{ - QMenu *menu = new QMenu(); - - connect(menu, &QMenu::aboutToShow, this, &AccountSettings::slotMenuBeforeShow); - - _addAccountAction = new QAction(tr("Add new"), this); - menu->addAction(_addAccountAction); - connect(_addAccountAction, &QAction::triggered, this, &AccountSettings::slotOpenAccountWizard); - - _toggleSignInOutAction = new QAction(tr("Log out"), this); - connect(_toggleSignInOutAction, &QAction::triggered, this, &AccountSettings::slotToggleSignInState); - menu->addAction(_toggleSignInOutAction); + connect(UserModel::instance(), &UserModel::addAccount, + this, &AccountSettings::slotOpenAccountWizard); - QAction *action = new QAction(tr("Remove"), this); - menu->addAction(action); - connect(action, &QAction::triggered, this, &AccountSettings::slotDeleteAccount); - - _ui->_accountToolbox->setText(tr("Account") + QLatin1Char(' ')); - _ui->_accountToolbox->setMenu(menu); - _ui->_accountToolbox->setPopupMode(QToolButton::InstantPopup); - - slotAccountAdded(_accountState); + customizeStyle(); } @@ -249,24 +223,6 @@ void AccountSettings::slotNewMnemonicGenerated() _ui->encryptionMessage->show(); } -void AccountSettings::slotMenuBeforeShow() { - if (_menuShown) { - return; - } - - auto menu = _ui->_accountToolbox->menu(); - - // We can't check this during the initial creation as there is no account yet then - if (_accountState->account()->capabilities().clientSideEncryptionAvaliable()) { - QAction *mnemonic = new QAction(tr("Show E2E mnemonic"), this); - connect(mnemonic, &QAction::triggered, this, &AccountSettings::requesetMnemonic); - menu->addAction(mnemonic); - } - - _menuShown = true; -} - - QString AccountSettings::selectedFolderAlias() const { QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex(); @@ -1060,21 +1016,12 @@ void AccountSettings::slotAccountStateChanged() // sync user interface buttons. refreshSelectiveSyncStatus(); - /* set the correct label for the Account toolbox button */ - if (_accountState) { - if (_accountState->isSignedOut()) { - _toggleSignInOutAction->setText(tr("Log in")); - } else { - _toggleSignInOutAction->setText(tr("Log out")); - } - } - if (state == AccountState::State::Connected) { /* TODO: We should probably do something better here. * Verify if the user has a private key already uploaded to the server, * if it has, do not offer to create one. */ - qCInfo(lcAccountSettings) << "Accout" << accountsState()->account()->displayName() + qCInfo(lcAccountSettings) << "Account" << accountsState()->account()->displayName() << "Client Side Encryption" << accountsState()->account()->capabilities().clientSideEncryptionAvaliable(); } } @@ -1190,18 +1137,6 @@ void AccountSettings::refreshSelectiveSyncStatus() } } -void AccountSettings::slotAccountAdded(AccountState *) -{ - // if the theme is limited to single account, the button must hide if - // there is already one account. - int s = AccountManager::instance()->accounts().size(); - if (s > 0 && !Theme::instance()->multiAccount()) { - _addAccountAction->setVisible(false); - } else { - _addAccountAction->setVisible(true); - } -} - void AccountSettings::slotDeleteAccount() { // Deleting the account potentially deletes 'this', so diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 886ebed2361a..e620029a5c30 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -90,7 +90,6 @@ protected slots: void slotDeleteAccount(); void slotToggleSignInState(); void slotOpenAccountWizard(); - void slotAccountAdded(AccountState *); void refreshSelectiveSyncStatus(); void slotMarkSubfolderEncrypted(const FolderStatusModel::SubFolderInfo* folderInfo); void slotMarkSubfolderDecrypted(const FolderStatusModel::SubFolderInfo* folderInfo); @@ -100,8 +99,6 @@ protected slots: void doExpand(); void slotLinkActivated(const QString &link); - void slotMenuBeforeShow(); - // Encryption Related Stuff. void slotShowMnemonic(const QString &mnemonic); void slotNewMnemonicGenerated(); diff --git a/src/gui/accountsettings.ui b/src/gui/accountsettings.ui index f52adf4c73b2..eafb4605d78e 100644 --- a/src/gui/accountsettings.ui +++ b/src/gui/accountsettings.ui @@ -6,7 +6,7 @@ 0 0 - 582 + 581 557 @@ -184,13 +184,6 @@ - - - - ... - - - diff --git a/src/gui/accountstate.cpp b/src/gui/accountstate.cpp index 7893a7a3b332..1491db714eed 100644 --- a/src/gui/accountstate.cpp +++ b/src/gui/accountstate.cpp @@ -20,6 +20,7 @@ #include "creds/httpcredentials.h" #include "logger.h" #include "configfile.h" +#include "ocsnavigationappsjob.h" #include #include @@ -27,6 +28,7 @@ #include #include +#include #include #include @@ -40,9 +42,9 @@ AccountState::AccountState(AccountPtr account) , _state(AccountState::Disconnected) , _connectionStatus(ConnectionValidator::Undefined) , _waitingForNewCredentials(false) - , _notificationsEtagResponseHeader("*") , _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay , _remoteWipe(new RemoteWipe(_account)) + , _hasTalk(false) { qRegisterMetaType("AccountState*"); @@ -74,6 +76,11 @@ AccountPtr AccountState::account() const return _account; } +bool AccountState::hasTalk() const +{ + return _hasTalk; +} + AccountState::ConnectionStatus AccountState::connectionStatus() const { return _connectionStatus; @@ -237,6 +244,9 @@ void AccountState::checkConnectivity() // Use a small authed propfind as a minimal ping when we're // already connected. conValidator->checkAuthentication(); + + // Get the Apps available on the server. + fetchNavigationApps(); } else { // Check the server and then the auth. @@ -267,7 +277,7 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta // Come online gradually from 503 or maintenance mode if (status == ConnectionValidator::Connected && (_connectionStatus == ConnectionValidator::ServiceUnavailable - || _connectionStatus == ConnectionValidator::MaintenanceMode)) { + || _connectionStatus == ConnectionValidator::MaintenanceMode)) { if (!_timeSinceMaintenanceOver.isValid()) { qCInfo(lcAccountState) << "AccountState reconnection: delaying for" << _maintenanceToConnectedDelay << "ms"; @@ -293,6 +303,9 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta case ConnectionValidator::Connected: if (_state != Connected) { setState(Connected); + + // Get the Apps available on the server. + fetchNavigationApps(); } break; case ConnectionValidator::Undefined: @@ -405,4 +418,110 @@ std::unique_ptr AccountState::settings() return s; } +void AccountState::fetchNavigationApps(){ + OcsNavigationAppsJob *job = new OcsNavigationAppsJob(_account); + job->addRawHeader("If-None-Match", navigationAppsEtagResponseHeader()); + connect(job, &OcsNavigationAppsJob::appsJobFinished, this, &AccountState::slotNavigationAppsFetched); + connect(job, &OcsNavigationAppsJob::etagResponseHeaderReceived, this, &AccountState::slotEtagResponseHeaderReceived); + connect(job, &OcsNavigationAppsJob::ocsError, this, &AccountState::slotOcsError); + job->getNavigationApps(); +} + +void AccountState::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){ + if(statusCode == 200){ + qCDebug(lcAccountState) << "New navigation apps ETag Response Header received " << value; + setNavigationAppsEtagResponseHeader(value); + } +} + +void AccountState::slotOcsError(int statusCode, const QString &message) +{ + qCDebug(lcAccountState) << "Error " << statusCode << " while fetching new navigation apps: " << message; +} + +void AccountState::slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode) +{ + if(_account){ + if (statusCode == 304) { + qCWarning(lcAccountState) << "Status code " << statusCode << " Not Modified - No new navigation apps."; + } else { + _apps.clear(); + _hasTalk = false; + + if(!reply.isEmpty()){ + auto element = reply.object().value("ocs").toObject().value("data"); + auto navLinks = element.toArray(); + + if(navLinks.size() > 0){ + foreach (const QJsonValue &value, navLinks) { + auto navLink = value.toObject(); + + AccountApp *app = new AccountApp(navLink.value("name").toString(), QUrl(navLink.value("href").toString()), + navLink.value("id").toString(), QUrl(navLink.value("icon").toString())); + + _apps << app; + + if(app->id() == QLatin1String("spreed")) + _hasTalk = true; + } + } + } + + emit hasFetchedNavigationApps(); + } + } +} + +AccountAppList AccountState::appList() const +{ + return _apps; +} + +AccountApp* AccountState::findApp(const QString &appId) const +{ + if(!appId.isEmpty()) { + foreach(AccountApp *app, appList()) { + if(app->id() == appId) + return app; + } + } + + return nullptr; +} + +/*-------------------------------------------------------------------------------------*/ + +AccountApp::AccountApp(const QString &name, const QUrl &url, + const QString &id, const QUrl &iconUrl, + QObject *parent) + : QObject(parent) + , _name(name) + , _url(url) + , _id(id) + , _iconUrl(iconUrl) +{ +} + +QString AccountApp::name() const +{ + return _name; +} + +QUrl AccountApp::url() const +{ + return _url; +} + +QString AccountApp::id() const +{ + return _id; +} + +QUrl AccountApp::iconUrl() const +{ + return _iconUrl; +} + +/*-------------------------------------------------------------------------------------*/ + } // namespace OCC diff --git a/src/gui/accountstate.h b/src/gui/accountstate.h index 6ed9aa265692..433fb51161b7 100644 --- a/src/gui/accountstate.h +++ b/src/gui/accountstate.h @@ -29,9 +29,11 @@ namespace OCC { class AccountState; class Account; +class AccountApp; class RemoteWipe; typedef QExplicitlySharedDataPointer AccountStatePtr; +typedef QList AccountAppList; /** * @brief Extra info about an ownCloud server account. @@ -101,6 +103,11 @@ class AccountState : public QObject, public QSharedData bool isSignedOut() const; + bool hasTalk() const; + + AccountAppList appList() const; + AccountApp* findApp(const QString &appId) const; + /** A user-triggered sign out which disconnects, stops syncs * for the account and forgets the password. */ void signOutByUi(); @@ -161,10 +168,12 @@ public slots: private: void setState(State state); + void fetchNavigationApps(); signals: void stateChanged(int state); void isConnectedChanged(); + void hasFetchedNavigationApps(); protected Q_SLOTS: void slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors); @@ -176,12 +185,17 @@ protected Q_SLOTS: void slotCredentialsFetched(AbstractCredentials *creds); void slotCredentialsAsked(AbstractCredentials *creds); + void slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode); + void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode); + void slotOcsError(int statusCode, const QString &message); + private: AccountPtr _account; State _state; ConnectionStatus _connectionStatus; QStringList _connectionErrors; bool _waitingForNewCredentials; + bool _hasTalk; QElapsedTimer _timeSinceLastETagCheck; QPointer _connectionValidator; QByteArray _notificationsEtagResponseHeader; @@ -205,7 +219,34 @@ protected Q_SLOTS: */ RemoteWipe *_remoteWipe; + /** + * Holds the App names and URLs available on the server + */ + AccountAppList _apps; + +}; + +class AccountApp : public QObject +{ + Q_OBJECT +public: + AccountApp(const QString &name, const QUrl &url, + const QString &id, const QUrl &iconUrl, + QObject* parent = 0); + + QString name() const; + QUrl url() const; + QString id() const; + QUrl iconUrl() const; + +private: + QString _name; + QUrl _url; + + QString _id; + QUrl _iconUrl; }; + } Q_DECLARE_METATYPE(OCC::AccountState *) diff --git a/src/gui/activityitemdelegate.cpp b/src/gui/activityitemdelegate.cpp deleted file mode 100644 index 2b4231e07292..000000000000 --- a/src/gui/activityitemdelegate.cpp +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright (C) by Klaas Freitag - * Copyright (C) by Olivier Goffart - * - * - * 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 "activityitemdelegate.h" -#include "activitylistmodel.h" -#include "folderstatusmodel.h" -#include "folderman.h" -#include "accountstate.h" -#include "activitydata.h" -#include -#include - -#include -#include -#include - -#define FIXME_USE_HIGH_DPI_RATIO -#ifdef FIXME_USE_HIGH_DPI_RATIO - // FIXME: Find a better way to calculate the text width on high-dpi displays (Retina). - #include -#endif - -#define HASQT5_11 (QT_VERSION >= QT_VERSION_CHECK(5,11,0)) - -namespace OCC { - -int ActivityItemDelegate::_iconHeight = 0; -int ActivityItemDelegate::_margin = 0; -int ActivityItemDelegate::_primaryButtonWidth = 0; -int ActivityItemDelegate::_secondaryButtonWidth = 0; -int ActivityItemDelegate::_spaceBetweenButtons = 0; -int ActivityItemDelegate::_timeWidth = 0; -int ActivityItemDelegate::_buttonHeight = 0; -const QString ActivityItemDelegate::_remote_share("remote_share"); -const QString ActivityItemDelegate::_call("call"); - -ActivityItemDelegate::ActivityItemDelegate() - : QStyledItemDelegate() -{ - customizeStyle(); -} - -int ActivityItemDelegate::iconHeight() -{ - if (_iconHeight == 0) { - QStyleOptionViewItem option; - QFont font = option.font; - - QFontMetrics fm(font); - - _iconHeight = qRound(fm.height() / 5.0 * 8.0); - } - return _iconHeight; -} - -int ActivityItemDelegate::rowHeight() -{ - if (_margin == 0) { - QStyleOptionViewItem opt; - - QFont f = opt.font; - QFontMetrics fm(f); - - _margin = fm.height() / 2; - -#if defined(Q_OS_WIN) - _margin += 5; -#endif - } - return iconHeight() + 5 * _margin; -} - -QSize ActivityItemDelegate::sizeHint(const QStyleOptionViewItem &option, - const QModelIndex & /* index */) const -{ - QFont font = option.font; - - return QSize(0, rowHeight()); -} - -void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - QStyledItemDelegate::paint(painter, option, index); - QFont font = option.font; - QFontMetrics fm(font); - int margin = fm.height() / 2.5; - painter->save(); - int iconSize = 16; - int iconOffset = qRound(fm.height() / 4.0 * 7.0); - int offset = 4; - const bool isSelected = (option.state & QStyle::State_Selected); -#ifdef FIXME_USE_HIGH_DPI_RATIO - // FIXME: Find a better way to calculate the text width on high-dpi displays (Retina). - const int device_pixel_ration = QApplication::desktop()->devicePixelRatio(); - int pixel_ratio = (device_pixel_ration > 1 ? device_pixel_ration : 1); -#endif - - // get the data - Activity::Type activityType = qvariant_cast(index.data(ActionRole)); - QIcon actionIcon; - const ActivityListModel::ActionIcon icn = qvariant_cast(index.data(ActionIconRole)); - switch(icn.iconType) { - case ActivityListModel::ActivityIconType::iconUseCached: actionIcon = icn.cachedIcon; break; - case ActivityListModel::ActivityIconType::iconActivity: actionIcon = (isSelected ? _iconActivity_sel : _iconActivity); break; - case ActivityListModel::ActivityIconType::iconBell: actionIcon = (isSelected ? _iconBell_sel : _iconBell); break; - case ActivityListModel::ActivityIconType::iconStateError: actionIcon = _iconStateError; break; - case ActivityListModel::ActivityIconType::iconStateWarning: actionIcon = _iconStateWarning; break; - case ActivityListModel::ActivityIconType::iconStateInfo: actionIcon = _iconStateInfo; break; - case ActivityListModel::ActivityIconType::iconStateSync: actionIcon = _iconStateSync; break; - } - QString objectType = qvariant_cast(index.data(ObjectTypeRole)); - QString actionText = qvariant_cast(index.data(ActionTextRole)); - QString messageText = qvariant_cast(index.data(MessageRole)); - QList customList = index.data(ActionsLinksRole).toList(); - QString timeText = qvariant_cast(index.data(PointInTimeRole)); - bool accountOnline = qvariant_cast(index.data(AccountConnectedRole)); - - // activity/notification icons - QRect actionIconRect = option.rect; - actionIconRect.setLeft(option.rect.left() + iconOffset/3); - actionIconRect.setRight(option.rect.left() + iconOffset); - actionIconRect.setTop(option.rect.top() + qRound((option.rect.height() - 16)/3.0)); - - // subject text rect - QRect actionTextBox = actionIconRect; -#if (HASQT5_11) - int actionTextBoxWidth = fm.horizontalAdvance(actionText); -#else - int actionTextBoxWidth = fm.width(actionText); -#endif - actionTextBox.setTop(option.rect.top() + margin + offset/2); - actionTextBox.setHeight(fm.height()); - actionTextBox.setLeft(actionIconRect.right() + margin); -#ifdef FIXME_USE_HIGH_DPI_RATIO - // FIXME: Find a better way to calculate the text width on high-dpi displays (Retina). - actionTextBoxWidth *= pixel_ratio; -#endif - actionTextBox.setRight(actionTextBox.left() + actionTextBoxWidth + margin); - - // message text rect - QRect messageTextBox = actionTextBox; -#if (HASQT5_11) - int messageTextWidth = fm.horizontalAdvance(messageText); -#else - int messageTextWidth = fm.width(messageText); -#endif - int messageTextTop = option.rect.top() + fm.height() + margin; - if(actionText.isEmpty()) messageTextTop = option.rect.top() + margin + offset/2; - messageTextBox.setTop(messageTextTop); - messageTextBox.setHeight(fm.height()); - messageTextBox.setBottom(messageTextBox.top() + fm.height()); - messageTextBox.setRight(messageTextBox.left() + messageTextWidth + margin); - if(messageText.isEmpty()){ - messageTextBox.setHeight(0); - messageTextBox.setBottom(messageTextBox.top()); - } - - // time box rect - QRect timeBox = messageTextBox; -#if (HASQT5_11) - int timeTextWidth = fm.horizontalAdvance(timeText); -#else - int timeTextWidth = fm.width(timeText); -#endif - int timeTop = option.rect.top() + fm.height() + fm.height() + margin + offset/2; - if(messageText.isEmpty() || actionText.isEmpty()) - timeTop = option.rect.top() + fm.height() + margin; - timeBox.setTop(timeTop); - timeBox.setHeight(fm.height()); - timeBox.setBottom(timeBox.top() + fm.height()); -#ifdef FIXME_USE_HIGH_DPI_RATIO - // FIXME: Find a better way to calculate the text width on high-dpi displays (Retina). - timeTextWidth *= pixel_ratio; -#endif - timeBox.setRight(timeBox.left() + timeTextWidth + margin); - - // buttons - default values - int rightMargin = margin; - int leftMargin = margin * offset; - int top = option.rect.top() + margin; - int buttonSize = option.rect.height()/2; - int right = option.rect.right() - rightMargin; - int left = right - buttonSize; - - QStyleOptionButton secondaryButton; - secondaryButton.rect = option.rect; - secondaryButton.features |= QStyleOptionButton::Flat; - secondaryButton.state |= QStyle::State_None; - secondaryButton.rect.setLeft(left); - secondaryButton.rect.setRight(right); - secondaryButton.rect.setTop(top + margin); - secondaryButton.rect.setHeight(iconSize); - - QStyleOptionButton primaryButton; - primaryButton.rect = option.rect; - primaryButton.features |= QStyleOptionButton::DefaultButton; - primaryButton.state |= QStyle::State_Raised; - primaryButton.rect.setTop(top); - primaryButton.rect.setHeight(buttonSize); - - right = secondaryButton.rect.left() - rightMargin; - left = secondaryButton.rect.left() - leftMargin; - - primaryButton.rect.setRight(right); - - if(activityType == Activity::Type::NotificationType){ - - // Secondary will be 'Dismiss' or '...' multiple options button - secondaryButton.icon = (isSelected ? _iconClose_sel : _iconClose); - if(customList.size() > 1) - secondaryButton.icon = (isSelected ? _iconMore_sel : _iconMore); - secondaryButton.iconSize = QSize(iconSize, iconSize); - - // Primary button will be 'More Information' or 'Accept' - primaryButton.text = tr("More information"); - if(objectType == _remote_share) primaryButton.text = tr("Accept"); - if(objectType == _call) primaryButton.text = tr("Join"); - -#if (HASQT5_11) - primaryButton.rect.setLeft(left - margin * 2 - fm.horizontalAdvance(primaryButton.text)); -#else - primaryButton.rect.setLeft(left - margin * 2 - fm.width(primaryButton.text)); -#endif - - // save info to be able to filter mouse clicks - _buttonHeight = buttonSize; - _primaryButtonWidth = primaryButton.rect.size().width(); - _secondaryButtonWidth = secondaryButton.rect.size().width(); - _spaceBetweenButtons = secondaryButton.rect.left() - primaryButton.rect.right(); - - } else if(activityType == Activity::SyncResultType){ - - // Secondary will be 'open file manager' with the folder icon - secondaryButton.icon = _iconFolder; - secondaryButton.iconSize = QSize(iconSize, iconSize); - - // Primary button will be 'open browser' - primaryButton.text = tr("Open Browser"); - -#if (HASQT5_11) - primaryButton.rect.setLeft(left - margin * 2 - fm.horizontalAdvance(primaryButton.text)); -#else - primaryButton.rect.setLeft(left - margin * 2 - fm.width(primaryButton.text)); -#endif - - // save info to be able to filter mouse clicks - _buttonHeight = buttonSize; - _primaryButtonWidth = primaryButton.rect.size().width(); - _secondaryButtonWidth = secondaryButton.rect.size().width(); - _spaceBetweenButtons = secondaryButton.rect.left() - primaryButton.rect.right(); - - } else if(activityType == Activity::SyncFileItemType){ - - // Secondary will be 'open file manager' with the folder icon - secondaryButton.icon = _iconFolder; - secondaryButton.iconSize = QSize(iconSize, iconSize); - - // No primary button on this case - // Whatever error we have at this case it is local, there is no point on opening the browser - _primaryButtonWidth = 0; - _secondaryButtonWidth = secondaryButton.rect.size().width(); - _spaceBetweenButtons = secondaryButton.rect.left() - primaryButton.rect.right(); - - } else { - _spaceBetweenButtons = leftMargin; - _primaryButtonWidth = 0; - _secondaryButtonWidth = 0; - } - - // draw the icon - QPixmap pm = actionIcon.pixmap(iconSize, iconSize, QIcon::Normal); - painter->drawPixmap(QPoint(actionIconRect.left(), actionIconRect.top()), pm); - - // change pen color if use is not online - QPalette p = option.palette; - if(!accountOnline) - p.setCurrentColorGroup(QPalette::Disabled); - - // change pen color if the line is selected - if (isSelected) - painter->setPen(p.color(QPalette::HighlightedText)); - else - painter->setPen(p.color(QPalette::Text)); - - // calculate space for text - use the max possible before using the elipses - int spaceLeftForText = option.rect.width() - (actionIconRect.width() + margin + rightMargin + leftMargin) - - (_primaryButtonWidth + _secondaryButtonWidth + _spaceBetweenButtons); - - // draw the subject - const QString elidedAction = fm.elidedText(actionText, Qt::ElideRight, spaceLeftForText); - painter->drawText(actionTextBox, elidedAction); - - // draw the buttons - if(activityType == Activity::Type::NotificationType || activityType == Activity::Type::SyncResultType) { - primaryButton.palette = p; - if (isSelected) - primaryButton.palette.setColor(QPalette::ButtonText, p.color(QPalette::HighlightedText)); - else - primaryButton.palette.setColor(QPalette::ButtonText, p.color(QPalette::Text)); - - QApplication::style()->drawControl(QStyle::CE_PushButton, &primaryButton, painter); - } - - // Since they are errors on local syncing, there is nothing to do in the server - if(activityType != Activity::Type::ActivityType) - QApplication::style()->drawControl(QStyle::CE_PushButton, &secondaryButton, painter); - - // draw the message - // change pen color for the message - if(!messageText.isEmpty()){ - const QString elidedMessage = fm.elidedText(messageText, Qt::ElideRight, spaceLeftForText); - painter->drawText(messageTextBox, elidedMessage); - } - - // change pen color for the time - if (isSelected) - painter->setPen(p.color(QPalette::Disabled, QPalette::HighlightedText)); - else - painter->setPen(p.color(QPalette::Disabled, QPalette::Text)); - - // draw the time - const QString elidedTime = fm.elidedText(timeText, Qt::ElideRight, spaceLeftForText); - painter->drawText(timeBox, elidedTime); - - painter->restore(); -} - -bool ActivityItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, - const QStyleOptionViewItem &option, const QModelIndex &index) -{ - Activity::Type activityType = qvariant_cast(index.data(ActionRole)); - if(activityType != Activity::Type::ActivityType){ - if (event->type() == QEvent::MouseButtonRelease){ - QMouseEvent *mouseEvent = (QMouseEvent*)event; - if(mouseEvent){ - int mouseEventX = mouseEvent->x(); - int mouseEventY = mouseEvent->y(); - int buttonsWidth = _primaryButtonWidth + _spaceBetweenButtons + _secondaryButtonWidth; - int x = option.rect.left() + option.rect.width() - buttonsWidth - _timeWidth; - int y = option.rect.top(); - - // clickable area for ... - if (mouseEventX > x && mouseEventX < x + buttonsWidth){ - if(mouseEventY > y && mouseEventY < y + _buttonHeight){ - - // ...primary button ('more information' or 'accept' on notifications or 'open browser' on errors) - if (mouseEventX > x && mouseEventX < x + _primaryButtonWidth){ - emit primaryButtonClickedOnItemView(index); - - // ...secondary button ('dismiss' on notifications or 'open file manager' on errors) - } else { - x += _primaryButtonWidth + _spaceBetweenButtons; - if (mouseEventX > x && mouseEventX < x + _secondaryButtonWidth) - emit secondaryButtonClickedOnItemView(index); - } - } - } - } - } - } - - return QStyledItemDelegate::editorEvent(event, model, option, index); -} - -void ActivityItemDelegate::slotStyleChanged() -{ - customizeStyle(); -} - -void ActivityItemDelegate::customizeStyle() -{ - QPalette pal; - pal.setColor(QPalette::Base, QColor(0,0,0)); // use dark background colour to invert icons - - _iconClose = Theme::createColorAwareIcon(QLatin1String(":/client/resources/close.svg")); - _iconClose_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/close.svg"), pal); - _iconMore = Theme::createColorAwareIcon(QLatin1String(":/client/resources/more.svg")); - _iconMore_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/more.svg"), pal); - - _iconFolder = QIcon(QLatin1String(":/client/resources/folder.svg")); - - _iconActivity = Theme::createColorAwareIcon(QLatin1String(":/client/resources/activity.png")); - _iconActivity_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/activity.png"), pal); - _iconBell = Theme::createColorAwareIcon(QLatin1String(":/client/resources/bell.svg")); - _iconBell_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/bell.svg"), pal); - - _iconStateError = QIcon(QLatin1String(":/client/resources/state-error.svg")); - _iconStateWarning = QIcon(QLatin1String(":/client/resources/state-warning.svg")); - _iconStateInfo = QIcon(QLatin1String(":/client/resources/state-info.svg")); - _iconStateSync = QIcon(QLatin1String(":/client/resources/state-sync.svg")); -} - -} // namespace OCC diff --git a/src/gui/activityitemdelegate.h b/src/gui/activityitemdelegate.h deleted file mode 100644 index 908889eee0be..000000000000 --- a/src/gui/activityitemdelegate.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) by Klaas Freitag - * Copyright (C) by Olivier Goffart - * - * 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. - */ - -#pragma once -#include -#include - -class QMouseEvent; - -namespace OCC { - -/** - * @brief The ActivityItemDelegate class - * @ingroup gui - */ -class ActivityItemDelegate : public QStyledItemDelegate -{ - Q_OBJECT -public: - enum datarole { ActionIconRole = Qt::UserRole + 1, - UserIconRole, - AccountRole, - ObjectTypeRole, - ActionsLinksRole, - ActionTextRole, - ActionRole, - MessageRole, - PathRole, - LinkRole, - PointInTimeRole, - AccountConnectedRole, - SyncFileStatusRole }; - - ActivityItemDelegate(); - - void paint(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const override; - QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override; - bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, - const QModelIndex &index) override; - - static int rowHeight(); - static int iconHeight(); - -public slots: - void slotStyleChanged(); - -signals: - void primaryButtonClickedOnItemView(const QModelIndex &index); - void secondaryButtonClickedOnItemView(const QModelIndex &index); - -private: - void customizeStyle(); - - static int _margin; - static int _iconHeight; - static int _primaryButtonWidth; - static int _secondaryButtonWidth; - static int _spaceBetweenButtons; - static int _timeWidth; - static int _buttonHeight; - static const QString _remote_share; - static const QString _call; - - QIcon _iconClose; - QIcon _iconClose_sel; - QIcon _iconMore; - QIcon _iconMore_sel; - - QIcon _iconFolder; - - QIcon _iconActivity; - QIcon _iconActivity_sel; - QIcon _iconBell; - QIcon _iconBell_sel; - - QIcon _iconStateError; - QIcon _iconStateWarning; - QIcon _iconStateInfo; - QIcon _iconStateSync; -}; - -} // namespace OCC diff --git a/src/gui/activitywidget.cpp b/src/gui/activitywidget.cpp deleted file mode 100644 index 469b0d14ea3d..000000000000 --- a/src/gui/activitywidget.cpp +++ /dev/null @@ -1,653 +0,0 @@ -/* - * Copyright (C) by Klaas Freitag - * - * 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 -#include - -#include "activitylistmodel.h" -#include "activitywidget.h" -#include "syncresult.h" -#include "logger.h" -#include "theme.h" -#include "folderman.h" -#include "syncfileitem.h" -#include "folder.h" -#include "openfilemanager.h" -#include "owncloudpropagator.h" -#include "account.h" -#include "accountstate.h" -#include "accountmanager.h" -#include "activityitemdelegate.h" -#include "QProgressIndicator.h" -#include "notificationconfirmjob.h" -#include "servernotificationhandler.h" -#include "theme.h" -#include "ocsjob.h" -#include "configfile.h" -#include "guiutility.h" -#include "socketapi.h" -#include "ui_activitywidget.h" -#include "syncengine.h" - -#include - -// time span in milliseconds which has to be between two -// refreshes of the notifications -#define NOTIFICATION_REQUEST_FREE_PERIOD 15000 - -namespace OCC { - -ActivityWidget::ActivityWidget(AccountState *accountState, QWidget *parent) - : QWidget(parent) - , _ui(new Ui::ActivityWidget) - , _notificationRequestsRunning(0) - , _accountState(accountState) - , _accept(tr("Accept")) - , _remote_share("remote_share") -{ - _ui->setupUi(this); - -// Adjust copyToClipboard() when making changes here! -#if defined(Q_OS_MAC) - _ui->_activityList->setMinimumWidth(400); -#endif - - _model = new ActivityListModel(accountState, this); - ActivityItemDelegate *delegate = new ActivityItemDelegate; - delegate->setParent(this); - _ui->_activityList->setItemDelegate(delegate); - _ui->_activityList->setAlternatingRowColors(true); - _ui->_activityList->setModel(_model); - - showLabels(); - - connect(_model, &ActivityListModel::activityJobStatusCode, - this, &ActivityWidget::slotAccountActivityStatus); - - connect(_model, &QAbstractItemModel::rowsInserted, this, &ActivityWidget::rowsInserted); - - connect(delegate, &ActivityItemDelegate::primaryButtonClickedOnItemView, this, &ActivityWidget::slotPrimaryButtonClickedOnListView); - connect(delegate, &ActivityItemDelegate::secondaryButtonClickedOnItemView, this, &ActivityWidget::slotSecondaryButtonClickedOnListView); - connect(_ui->_activityList, &QListView::activated, this, &ActivityWidget::slotOpenFile); - - connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo, - this, &ActivityWidget::slotProgressInfo); - connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted, - this, &ActivityWidget::slotItemCompleted); - connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError, - this, &ActivityWidget::addError); - - _removeTimer.setInterval(1000); - - // Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching) - connect(this, &ActivityWidget::styleChanged, delegate, &ActivityItemDelegate::slotStyleChanged); -} - -ActivityWidget::~ActivityWidget() -{ - delete _ui; -} - -void ActivityWidget::slotProgressInfo(const QString &folder, const ProgressInfo &progress) -{ - if (progress.status() == ProgressInfo::Reconcile) { - // Wipe all non-persistent entries - as well as the persistent ones - // in cases where a local discovery was done. - auto f = FolderMan::instance()->folder(folder); - if (!f) - return; - const auto &engine = f->syncEngine(); - const auto style = engine.lastLocalDiscoveryStyle(); - foreach (Activity activity, _model->errorsList()) { - if (activity._folder != folder){ - continue; - } - - if (style == LocalDiscoveryStyle::FilesystemOnly){ - _model->removeActivityFromActivityList(activity); - continue; - } - - if(activity._status == SyncFileItem::Conflict && !QFileInfo(f->path() + activity._file).exists()){ - _model->removeActivityFromActivityList(activity); - continue; - } - - if(activity._status == SyncFileItem::FileLocked && !QFileInfo(f->path() + activity._file).exists()){ - _model->removeActivityFromActivityList(activity); - continue; - } - - - if(activity._status == SyncFileItem::FileIgnored && !QFileInfo(f->path() + activity._file).exists()) { - _model->removeActivityFromActivityList(activity); - continue; - } - - - if(!QFileInfo(f->path() + activity._file).exists()){ - _model->removeActivityFromActivityList(activity); - continue; - } - - auto path = QFileInfo(activity._file).dir().path().toUtf8(); - if (path == ".") - path.clear(); - - if(engine.shouldDiscoverLocally(path)) - _model->removeActivityFromActivityList(activity); - } - - } - - if (progress.status() == ProgressInfo::Done) { - // We keep track very well of pending conflicts. - // Inform other components about them. - QStringList conflicts; - foreach (Activity activity, _model->errorsList()) { - if (activity._folder == folder - && activity._status == SyncFileItem::Conflict) { - conflicts.append(activity._file); - } - } - - emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts); - } -} - -void ActivityWidget::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item){ - auto folderInstance = FolderMan::instance()->folder(folder); - - if (!folderInstance) - return; - - // check if we are adding it to the right account and if it is useful information (protocol errors) - if(folderInstance->accountState() == _accountState){ - qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in " << item->_errorString; - - Activity activity; - activity._type = Activity::SyncFileItemType; //client activity - activity._status = item->_status; - activity._dateTime = QDateTime::currentDateTime(); - activity._message = item->_originalFile; - activity._link = folderInstance->accountState()->account()->url(); - activity._accName = folderInstance->accountState()->account()->displayName(); - activity._file = item->_file; - activity._folder = folder; - - if(item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success){ - qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully."; - activity._message.prepend(" "); - activity._message.prepend(tr("Synced")); - _model->addSyncFileItemToActivityList(activity); - } else { - qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString; - activity._subject = item->_errorString; - - if(item->_status == SyncFileItem::Status::FileIgnored) { - _model->addIgnoredFileToList(activity); - } else { - // add 'protocol error' to activity list - _model->addErrorToActivityList(activity); - } - } - } -} - -void ActivityWidget::addError(const QString &folderAlias, const QString &message, - ErrorCategory category) -{ - auto folderInstance = FolderMan::instance()->folder(folderAlias); - if (!folderInstance) - return; - - if(folderInstance->accountState() == _accountState){ - qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << message; - - Activity activity; - activity._type = Activity::SyncResultType; - activity._status = SyncResult::Error; - activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate); - activity._subject = message; - activity._message = folderInstance->shortGuiLocalPath(); - activity._link = folderInstance->shortGuiLocalPath(); - activity._accName = folderInstance->accountState()->account()->displayName(); - activity._folder = folderAlias; - - - if (category == ErrorCategory::InsufficientRemoteStorage) { - ActivityLink link; - link._label = tr("Retry all uploads"); - link._link = folderInstance->path(); - link._verb = ""; - link._isPrimary = true; - activity._links.append(link); - } - - // add 'other errors' to activity list - _model->addErrorToActivityList(activity); - } -} - - -void ActivityWidget::slotPrimaryButtonClickedOnListView(const QModelIndex &index){ - QUrl link = qvariant_cast(index.data(ActivityItemDelegate::LinkRole)); - QString objectType = index.data(ActivityItemDelegate::ObjectTypeRole).toString(); - if(!link.isEmpty()){ - qCWarning(lcActivity) << "Opening" << link.toString() << "in browser for Notification/Activity" << qvariant_cast(index.data(ActivityItemDelegate::ActionTextRole)); - Utility::openBrowser(link, this); - } else if(objectType == _remote_share){ - QVariant customItem = index.data(ActivityItemDelegate::ActionsLinksRole).toList().first(); - ActivityLink actionLink = qvariant_cast(customItem); - if(actionLink._label == _accept){ - qCWarning(lcActivity) << objectType << "action" << actionLink._label << "for" << qvariant_cast(index.data(ActivityItemDelegate::ActionTextRole)); - const QString accountName = index.data(ActivityItemDelegate::AccountRole).toString(); - slotSendNotificationRequest(accountName, actionLink._link, actionLink._verb, index.row()); - } else { - qCWarning(lcActivity) << "Failed: " << objectType << "action" << actionLink._label << "for" << qvariant_cast(index.data(ActivityItemDelegate::ActionTextRole)); - } - } -} - -void ActivityWidget::slotSecondaryButtonClickedOnListView(const QModelIndex &index){ - QList customList = index.data(ActivityItemDelegate::ActionsLinksRole).toList(); - QString objectType = index.data(ActivityItemDelegate::ObjectTypeRole).toString(); - - QList actionLinks; - foreach(QVariant customItem, customList){ - actionLinks << qvariant_cast(customItem); - } - - if(objectType == _remote_share && actionLinks.first()._label == _accept) - actionLinks.removeFirst(); - - if(qvariant_cast(index.data(ActivityItemDelegate::ActionRole)) == Activity::Type::NotificationType){ - const QString accountName = index.data(ActivityItemDelegate::AccountRole).toString(); - if(actionLinks.size() == 1){ - if(actionLinks.at(0)._verb == "DELETE"){ - qCWarning(lcActivity) << "Dismissing Notification/Activity" << qvariant_cast(index.data(ActivityItemDelegate::ActionTextRole)); - slotSendNotificationRequest(index.data(ActivityItemDelegate::AccountRole).toString(), actionLinks.at(0)._link, actionLinks.at(0)._verb, index.row()); - } - } else if(actionLinks.size() > 1){ - QMenu menu; - qCWarning(lcActivity) << "Displaying menu for Notification/Activity" << qvariant_cast(index.data(ActivityItemDelegate::ActionTextRole)); - foreach (ActivityLink actionLink, actionLinks) { - QAction *menuAction = new QAction(actionLink._label, &menu); - connect(menuAction, &QAction::triggered, this, [this, index, accountName, actionLink] { - this->slotSendNotificationRequest(accountName, actionLink._link, actionLink._verb, index.row()); - }); - menu.addAction(menuAction); - } - menu.exec(QCursor::pos()); - } - } - - Activity::Type activityType = qvariant_cast(index.data(ActivityItemDelegate::ActionRole)); - if(activityType == Activity::Type::SyncFileItemType || activityType == Activity::Type::SyncResultType) - slotOpenFile(index); -} - -void ActivityWidget::slotNotificationRequestFinished(int statusCode) -{ - int row = sender()->property("activityRow").toInt(); - - // the ocs API returns stat code 100 or 200 inside the xml if it succeeded. - if (statusCode != OCS_SUCCESS_STATUS_CODE && statusCode != OCS_SUCCESS_STATUS_CODE_V2) { - qCWarning(lcActivity) << "Notification Request to Server failed, leave notification visible."; - } else { - // to do use the model to rebuild the list or remove the item - qCWarning(lcActivity) << "Notification Request to Server successed, rebuilding list."; - _model->removeActivityFromActivityList(row); - } -} - -void ActivityWidget::slotRefreshActivities() -{ - _model->slotRefreshActivity(); -} - -void ActivityWidget::slotRefreshNotifications() -{ - // start a server notification handler if no notification requests - // are running - if (_notificationRequestsRunning == 0) { - ServerNotificationHandler *snh = new ServerNotificationHandler(_accountState); - connect(snh, &ServerNotificationHandler::newNotificationList, - this, &ActivityWidget::slotBuildNotificationDisplay); - - snh->slotFetchNotifications(); - } else { - qCWarning(lcActivity) << "Notification request counter not zero."; - } -} - -void ActivityWidget::slotRemoveAccount() -{ - _model->slotRemoveAccount(); -} - -void ActivityWidget::showLabels() -{ - _ui->_bottomLabel->hide(); // hide whatever was there before - QString t(""); - QSetIterator i(_accountsWithoutActivities); - while (i.hasNext()) { - t.append(tr("
Account %1 does not have activities enabled.").arg(i.next())); - } - if(!t.isEmpty()){ - _ui->_bottomLabel->setTextFormat(Qt::RichText); - _ui->_bottomLabel->setText(t); - _ui->_bottomLabel->show(); - } -} - -void ActivityWidget::slotAccountActivityStatus(int statusCode) -{ - if (!(_accountState && _accountState->account())) { - return; - } - if (statusCode == 999) { - _accountsWithoutActivities.insert(_accountState->account()->displayName()); - } else { - _accountsWithoutActivities.remove(_accountState->account()->displayName()); - } - - checkActivityWidgetVisibility(); - showLabels(); -} - -// FIXME: Reused from protocol widget. Move over to utilities. -QString ActivityWidget::timeString(QDateTime dt, QLocale::FormatType format) const -{ - const QLocale loc = QLocale::system(); - QString dtFormat = loc.dateTimeFormat(format); - static const QRegExp re("(HH|H|hh|h):mm(?!:s)"); - dtFormat.replace(re, "\\1:mm:ss"); - return loc.toString(dt, dtFormat); -} - -void ActivityWidget::storeActivityList(QTextStream &ts) -{ - ActivityList activities = _model->activityList(); - - foreach (Activity activity, activities) { - ts << right - // account name - << qSetFieldWidth(activity._accName.length()) - << activity._accName - // separator - << qSetFieldWidth(2) << " - " - - // date and time - << qSetFieldWidth(activity._dateTime.toString().length()) - << activity._dateTime.toString() - // separator - << qSetFieldWidth(2) << " - " - - // fileq - << qSetFieldWidth(activity._file.length()) - << activity._file - // separator - << qSetFieldWidth(2) << " - " - - // subject - << qSetFieldWidth(activity._subject.length()) - << activity._subject - // separator - << qSetFieldWidth(2) << " - " - - // message - << qSetFieldWidth(activity._message.length()) - << activity._message - << endl; - } -} - -void ActivityWidget::checkActivityWidgetVisibility() -{ - int accountCount = AccountManager::instance()->accounts().count(); - bool hasAccountsWithActivity = - _accountsWithoutActivities.count() != accountCount; - - _ui->_activityList->setVisible(hasAccountsWithActivity); - - emit hideActivityTab(!hasAccountsWithActivity); -} - -void ActivityWidget::slotOpenFile(QModelIndex indx) -{ - qCDebug(lcActivity) << indx.isValid() << indx.data(ActivityItemDelegate::PathRole).toString() << QFile::exists(indx.data(ActivityItemDelegate::PathRole).toString()); - if (indx.isValid()) { - QString fullPath = indx.data(ActivityItemDelegate::PathRole).toString(); - if(!fullPath.isEmpty()){ - if (QFile::exists(fullPath)) { - showInFileManager(fullPath); - } - } - } -} - -// GUI: Display the notifications. -// All notifications in list are coming from the same account -// but in the _widgetForNotifId hash widgets for all accounts are -// collected. -void ActivityWidget::slotBuildNotificationDisplay(const ActivityList &list) -{ - // Whether a new notification was added to the list - bool newNotificationShown = false; - - _model->clearNotifications(); - - foreach (auto activity, list) { - if (_blacklistedNotifications.contains(activity)) { - qCInfo(lcActivity) << "Activity in blacklist, skip"; - continue; - } - - // handle gui logs. In order to NOT annoy the user with every fetching of the - // notifications the notification id is stored in a Set. Only if an id - // is not in the set, it qualifies for guiLog. - // Important: The _guiLoggedNotifications set must be wiped regularly which - // will repeat the gui log. - - // after one hour, clear the gui log notification store - if (_guiLogTimer.elapsed() > 60 * 60 * 1000) { - _guiLoggedNotifications.clear(); - } - - if (!_guiLoggedNotifications.contains(activity._id)) { - newNotificationShown = true; - _guiLoggedNotifications.insert(activity._id); - - // Assemble a tray notification for the NEW notification - ConfigFile cfg; - if(cfg.optionalServerNotifications()){ - if(AccountManager::instance()->accounts().count() == 1){ - emit guiLog(activity._subject, ""); - } else { - emit guiLog(activity._subject, activity._accName); - } - } - } - - _model->addNotificationToActivityList(activity); - } - - // restart the gui log timer now that we show a new notification - if(newNotificationShown) { - _guiLogTimer.start(); - } -} - -void ActivityWidget::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row) -{ - qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName; - - const QStringList validVerbs = QStringList() << "GET" - << "PUT" - << "POST" - << "DELETE"; - - if (validVerbs.contains(verb)) { - AccountStatePtr acc = AccountManager::instance()->account(accountName); - if (acc) { - NotificationConfirmJob *job = new NotificationConfirmJob(acc->account()); - QUrl l(link); - job->setLinkAndVerb(l, verb); - job->setProperty("activityRow", QVariant::fromValue(row)); - connect(job, &AbstractNetworkJob::networkError, - this, &ActivityWidget::slotNotifyNetworkError); - connect(job, &NotificationConfirmJob::jobFinished, - this, &ActivityWidget::slotNotifyServerFinished); - job->start(); - - // count the number of running notification requests. If this member var - // is larger than zero, no new fetching of notifications is started - _notificationRequestsRunning++; - } - } else { - qCWarning(lcActivity) << "Notification Links: Invalid verb:" << verb; - } -} - -void ActivityWidget::endNotificationRequest(int replyCode) -{ - _notificationRequestsRunning--; - slotNotificationRequestFinished(replyCode); -} - -void ActivityWidget::slotNotifyNetworkError(QNetworkReply *reply) -{ - NotificationConfirmJob *job = qobject_cast(sender()); - if (!job) { - return; - } - - int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - endNotificationRequest(resultCode); - qCWarning(lcActivity) << "Server notify job failed with code " << resultCode; -} - -void ActivityWidget::slotNotifyServerFinished(const QString &reply, int replyCode) -{ - NotificationConfirmJob *job = qobject_cast(sender()); - if (!job) { - return; - } - - endNotificationRequest(replyCode); - qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply; -} - -void ActivityWidget::slotStyleChanged() -{ - // Notify the other widgets (Dark-/Light-Mode switching) - emit styleChanged(); -} - -/* ==================================================================== */ - -ActivitySettings::ActivitySettings(AccountState *accountState, QWidget *parent) - : QWidget(parent) - , _accountState(accountState) -{ - _vbox = new QVBoxLayout(this); - setLayout(_vbox); - - _activityWidget = new ActivityWidget(_accountState, this); - - _vbox->insertWidget(1, _activityWidget); - connect(_activityWidget, &ActivityWidget::guiLog, this, &ActivitySettings::guiLog); - connect(&_notificationCheckTimer, &QTimer::timeout, - this, &ActivitySettings::slotRegularNotificationCheck); - - // Add a progress indicator to spin if the acitivity list is updated. - _progressIndicator = new QProgressIndicator(this); - - // connect a model signal to stop the animation - connect(_activityWidget, &ActivityWidget::rowsInserted, _progressIndicator, &QProgressIndicator::stopAnimation); - connect(_activityWidget, &ActivityWidget::rowsInserted, this, &ActivitySettings::slotDisplayActivities); - - // Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching) - connect(this, &ActivitySettings::styleChanged, _activityWidget, &ActivityWidget::slotStyleChanged); -} - -void ActivitySettings::slotDisplayActivities(){ - _vbox->removeWidget(_progressIndicator); -} - -void ActivitySettings::setNotificationRefreshInterval(std::chrono::milliseconds interval) -{ - qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval"; - _notificationCheckTimer.start(interval.count()); -} - -void ActivitySettings::slotRemoveAccount() -{ - _activityWidget->slotRemoveAccount(); -} - -void ActivitySettings::slotRefresh() -{ - // QElapsedTimer isn't actually constructed as invalid. - if (!_timeSinceLastCheck.contains(_accountState)) { - _timeSinceLastCheck[_accountState].invalidate(); - } - QElapsedTimer &timer = _timeSinceLastCheck[_accountState]; - - // Fetch Activities only if visible and if last check is longer than 15 secs ago - if (timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD) { - qCDebug(lcActivity) << "Do not check as last check is only secs ago: " << timer.elapsed() / 1000; - return; - } - if (_accountState && _accountState->isConnected()) { - if (isVisible() || !timer.isValid()) { - _vbox->insertWidget(0, _progressIndicator); - _vbox->setAlignment(_progressIndicator, Qt::AlignHCenter); - _progressIndicator->startAnimation(); - _activityWidget->slotRefreshActivities(); - } - _activityWidget->slotRefreshNotifications(); - timer.start(); - } -} - -void ActivitySettings::slotRegularNotificationCheck() -{ - slotRefresh(); -} - -bool ActivitySettings::event(QEvent *e) -{ - if (e->type() == QEvent::Show) { - slotRefresh(); - } - return QWidget::event(e); -} - -ActivitySettings::~ActivitySettings() -{ -} - -void ActivitySettings::slotStyleChanged() -{ - if(_progressIndicator) - _progressIndicator->setColor(QGuiApplication::palette().color(QPalette::Text)); - - // Notify the other widgets (Dark-/Light-Mode switching) - emit styleChanged(); -} - -} diff --git a/src/gui/activitywidget.h b/src/gui/activitywidget.h deleted file mode 100644 index ee271c62a10f..000000000000 --- a/src/gui/activitywidget.h +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) by Klaas Freitag - * - * 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. - */ - -#ifndef ACTIVITYWIDGET_H -#define ACTIVITYWIDGET_H - -#include -#include -#include -#include -#include - -#include "progressdispatcher.h" -#include "owncloudgui.h" -#include "account.h" -#include "activitydata.h" -#include "accountmanager.h" - -#include "ui_activitywidget.h" - -class QPushButton; -class QProgressIndicator; - -namespace OCC { - -class Account; -class AccountStatusPtr; -class JsonApiJob; -class ActivityListModel; - -namespace Ui { - class ActivityWidget; -} -class Application; - -/** - * @brief The ActivityWidget class - * @ingroup gui - * - * The list widget to display the activities, contained in the - * subsequent ActivitySettings widget. - */ - -class ActivityWidget : public QWidget -{ - Q_OBJECT -public: - explicit ActivityWidget(AccountState *accountState, QWidget *parent = nullptr); - ~ActivityWidget(); - QSize sizeHint() const override { return ownCloudGui::settingsDialogSize(); } - void storeActivityList(QTextStream &ts); - - /** - * Adjusts the activity tab's and some widgets' visibility - * - * Based on whether activities are enabled and whether notifications are - * available. - */ - void checkActivityWidgetVisibility(); - -public slots: - void slotOpenFile(QModelIndex indx); - void slotRefreshActivities(); - void slotRefreshNotifications(); - void slotRemoveAccount(); - void slotAccountActivityStatus(int statusCode); - void addError(const QString &folderAlias, const QString &message, ErrorCategory category); - void slotProgressInfo(const QString &folder, const ProgressInfo &progress); - void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item); - void slotStyleChanged(); - -signals: - void guiLog(const QString &, const QString &); - void rowsInserted(); - void hideActivityTab(bool); - void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row); - void styleChanged(); - -private slots: - void slotBuildNotificationDisplay(const ActivityList &list); - void slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row); - void slotNotifyNetworkError(QNetworkReply *); - void slotNotifyServerFinished(const QString &reply, int replyCode); - void endNotificationRequest(int replyCode); - void slotNotificationRequestFinished(int statusCode); - void slotPrimaryButtonClickedOnListView(const QModelIndex &index); - void slotSecondaryButtonClickedOnListView(const QModelIndex &index); - -private: - void customizeStyle(); - void showLabels(); - QString timeString(QDateTime dt, QLocale::FormatType format) const; - Ui::ActivityWidget *_ui; - QSet _accountsWithoutActivities; - QElapsedTimer _guiLogTimer; - QSet _guiLoggedNotifications; - ActivityList _blacklistedNotifications; - - QTimer _removeTimer; - - // number of currently running notification requests. If non zero, - // no query for notifications is started. - int _notificationRequestsRunning; - - ActivityListModel *_model; - AccountState *_accountState; - const QString _accept; - const QString _remote_share; -}; - - -/** - * @brief The ActivitySettings class - * @ingroup gui - * - * Implements a tab for the settings dialog, displaying the three activity - * lists. - */ -class ActivitySettings : public QWidget -{ - Q_OBJECT -public: - explicit ActivitySettings(AccountState *accountState, QWidget *parent = nullptr); - - ~ActivitySettings(); - QSize sizeHint() const override { return ownCloudGui::settingsDialogSize(); } - -public slots: - void slotRefresh(); - void slotRemoveAccount(); - void setNotificationRefreshInterval(std::chrono::milliseconds interval); - void slotStyleChanged(); - -private slots: - void slotRegularNotificationCheck(); - void slotDisplayActivities(); - -signals: - void guiLog(const QString &, const QString &); - void styleChanged(); - -private: - bool event(QEvent *e) override; - - ActivityWidget *_activityWidget; - QProgressIndicator *_progressIndicator; - QVBoxLayout *_vbox; - QTimer _notificationCheckTimer; - QHash _timeSinceLastCheck; - - AccountState *_accountState; -}; -} -#endif // ActivityWIDGET_H diff --git a/src/gui/activitywidget.ui b/src/gui/activitywidget.ui deleted file mode 100644 index 7e5e9f73c49e..000000000000 --- a/src/gui/activitywidget.ui +++ /dev/null @@ -1,98 +0,0 @@ - - - OCC::ActivityWidget - - - - 0 - 0 - 652 - 556 - - - - Form - - - - QLayout::SetDefaultConstraint - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - 1 - - - QAbstractScrollArea::AdjustToContents - - - QAbstractItemView::NoEditTriggers - - - false - - - Qt::IgnoreAction - - - QListView::Adjust - - - QListView::ListMode - - - 0 - - - true - - - - - - - - 0 - 0 - - - - TextLabel - - - Qt::RichText - - - - - - - _activityList - - - - diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 51af86f92354..4cfcab31d265 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -266,6 +266,8 @@ Application::Application(int &argc, char **argv) // Allow other classes to hook into isShowingSettingsDialog() signals (re-auth widgets, for example) connect(_gui.data(), &ownCloudGui::isShowingSettingsDialog, this, &Application::slotGuiIsShowingSettings); + + _gui->createTray(); } Application::~Application() @@ -390,7 +392,7 @@ void Application::slotownCloudWizardDone(int res) Utility::setLaunchOnStartup(_theme->appName(), _theme->appNameGUI(), true); } - _gui->slotShowSettings(); + Systray::instance()->showWindow(); } } diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 0e906a4203c6..f9cb8cfe00f6 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -914,6 +914,8 @@ void Folder::slotItemCompleted(const SyncFileItemPtr &item) _folderWatcher->removePath(path() + item->_file); _folderWatcher->addPath(path() + item->destination()); break; + default: + break; } } diff --git a/src/gui/generalsettings.ui b/src/gui/generalsettings.ui index fcdfc073c9be..1c7c25015893 100644 --- a/src/gui/generalsettings.ui +++ b/src/gui/generalsettings.ui @@ -6,7 +6,7 @@ 0 0 - 785 + 516 523 diff --git a/src/gui/iconjob.cpp b/src/gui/iconjob.cpp index c77a925b18a4..69f84d4bd791 100644 --- a/src/gui/iconjob.cpp +++ b/src/gui/iconjob.cpp @@ -23,6 +23,7 @@ IconJob::IconJob(const QUrl &url, QObject *parent) : this, &IconJob::finished); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); _accessManager.get(request); } diff --git a/src/gui/networksettings.ui b/src/gui/networksettings.ui index 5d7dc94e8781..b9391cf7e59d 100644 --- a/src/gui/networksettings.ui +++ b/src/gui/networksettings.ui @@ -6,7 +6,7 @@ 0 0 - 563 + 516 444 diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 828409f900f4..f4f37ba1ae74 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -14,7 +14,6 @@ #include "application.h" #include "owncloudgui.h" -#include "ocsnavigationappsjob.h" #include "theme.h" #include "folderman.h" #include "progressdispatcher.h" @@ -46,9 +45,11 @@ #include #endif -#include -#include -#include +#include +#include +#include +#include +#include namespace OCC { @@ -62,22 +63,32 @@ ownCloudGui::ownCloudGui(Application *parent) #ifdef WITH_LIBCLOUDPROVIDERS , _bus(QDBusConnection::sessionBus()) #endif - , _recentActionsMenu(nullptr) , _app(parent) { - _tray = new Systray(); + _tray = Systray::instance(); _tray->setParent(this); - // for the beginning, set the offline icon until the account was verified - _tray->setIcon(Theme::instance()->folderOfflineIcon(/*systray?*/ true, /*currently visible?*/ false)); + _tray->setIcon(Theme::instance()->folderOfflineIcon(/*systray?*/ true)); + + _tray->show(); connect(_tray.data(), &QSystemTrayIcon::activated, this, &ownCloudGui::slotTrayClicked); - setupActions(); - setupContextMenu(); + connect(_tray.data(), &Systray::pauseSync, + this, &ownCloudGui::slotPauseAllFolders); - _tray->show(); + connect(_tray.data(), &Systray::pauseSync, + this, &ownCloudGui::slotUnpauseAllFolders); + + connect(_tray.data(), &Systray::openHelp, + this, &ownCloudGui::slotHelp); + + connect(_tray.data(), &Systray::openSettings, + this, &ownCloudGui::slotShowSettings); + + connect(_tray.data(), &Systray::shutdown, + this, &ownCloudGui::slotShutdown); ProgressDispatcher *pd = ProgressDispatcher::instance(); connect(pd, &ProgressDispatcher::progressInfo, this, @@ -87,17 +98,18 @@ ownCloudGui::ownCloudGui(Application *parent) connect(folderMan, &FolderMan::folderSyncStateChange, this, &ownCloudGui::slotSyncStateChange); - connect(AccountManager::instance(), &AccountManager::accountAdded, - this, &ownCloudGui::updateContextMenuNeeded); - connect(AccountManager::instance(), &AccountManager::accountRemoved, - this, &ownCloudGui::updateContextMenuNeeded); - 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 @@ -140,13 +152,6 @@ void ownCloudGui::slotOpenSettingsDialog() void ownCloudGui::slotTrayClicked(QSystemTrayIcon::ActivationReason reason) { - if (_workaroundFakeDoubleClick) { - static QElapsedTimer last_click; - if (last_click.isValid() && last_click.elapsed() < 200) { - return; - } - last_click.start(); - } // Left click if (reason == QSystemTrayIcon::Trigger) { @@ -158,17 +163,10 @@ void ownCloudGui::slotTrayClicked(QSystemTrayIcon::ActivationReason reason) Q_ASSERT(shareDialog.data()); raiseDialog(shareDialog); } + } else if (_tray->isOpen()) { + _tray->hideWindow(); } else { -#ifdef Q_OS_MAC - // on macOS, a left click always opens menu. - // However if the settings dialog is already visible but hidden - // by other applications, this will bring it to the front. - if (!_settingsDialog.isNull() && _settingsDialog->isVisible()) { - raiseDialog(_settingsDialog.data()); - } -#else - slotOpenSettingsDialog(); -#endif + _tray->showWindow(); } } // FIXME: Also make sure that any auto updater dialogue https://github.com/owncloud/client/issues/5613 @@ -178,7 +176,6 @@ void ownCloudGui::slotTrayClicked(QSystemTrayIcon::ActivationReason reason) void ownCloudGui::slotSyncStateChange(Folder *folder) { slotComputeOverallSyncStatus(); - updateContextMenuNeeded(); if (!folder) { return; // Valid, just a general GUI redraw was needed. @@ -194,16 +191,11 @@ void ownCloudGui::slotSyncStateChange(Folder *folder) || result.status() == SyncResult::Error) { Logger::instance()->enterNextLogFile(); } - - if (result.status() == SyncResult::NotYetStarted) { - _settingsDialog->slotRefreshActivity(folder->accountState()); - } } void ownCloudGui::slotFoldersChanged() { slotComputeOverallSyncStatus(); - updateContextMenuNeeded(); } void ownCloudGui::slotOpenPath(const QString &path) @@ -213,7 +205,6 @@ void ownCloudGui::slotOpenPath(const QString &path) void ownCloudGui::slotAccountStateChanged() { - updateContextMenuNeeded(); slotComputeOverallSyncStatus(); } @@ -239,7 +230,7 @@ void ownCloudGui::slotComputeOverallSyncStatus() // Don't overwrite the status if we're currently syncing if (FolderMan::instance()->currentSyncFolder()) return; - _actionStatus->setText(text); + //_actionStatus->setText(text); }; foreach (auto a, AccountManager::instance()->accounts()) { @@ -259,7 +250,7 @@ void ownCloudGui::slotComputeOverallSyncStatus() } if (!problemAccounts.empty()) { - _tray->setIcon(Theme::instance()->folderOfflineIcon(true, contextMenuVisible())); + _tray->setIcon(Theme::instance()->folderOfflineIcon(true)); if (allDisconnected) { setStatusText(tr("Disconnected")); } else { @@ -289,12 +280,12 @@ void ownCloudGui::slotComputeOverallSyncStatus() } if (allSignedOut) { - _tray->setIcon(Theme::instance()->folderOfflineIcon(true, contextMenuVisible())); + _tray->setIcon(Theme::instance()->folderOfflineIcon(true)); _tray->setToolTip(tr("Please sign in")); setStatusText(tr("Signed out")); return; } else if (allPaused) { - _tray->setIcon(Theme::instance()->syncStateIcon(SyncResult::Paused, true, contextMenuVisible())); + _tray->setIcon(Theme::instance()->syncStateIcon(SyncResult::Paused, true)); _tray->setToolTip(tr("Account synchronization is disabled")); setStatusText(tr("Synchronization is paused")); return; @@ -321,7 +312,7 @@ void ownCloudGui::slotComputeOverallSyncStatus() iconStatus = SyncResult::Problem; } - QIcon statusIcon = Theme::instance()->syncStateIcon(iconStatus, true, contextMenuVisible()); + QIcon statusIcon = Theme::instance()->syncStateIcon(iconStatus, true); _tray->setIcon(statusIcon); // create the tray blob message, check if we have an defined state @@ -359,381 +350,6 @@ void ownCloudGui::slotComputeOverallSyncStatus() } } -void ownCloudGui::addAccountContextMenu(AccountStatePtr accountState, QMenu *menu, bool separateMenu) -{ - // Only show the name in the action if it's not part of an - // account sub menu. - QString browserOpen = tr("Open in browser"); - if (!separateMenu) { - browserOpen = tr("Open %1 in browser").arg(Theme::instance()->appNameGUI()); - } - auto actionOpenoC = menu->addAction(browserOpen); - actionOpenoC->setProperty(propertyAccountC, QVariant::fromValue(accountState->account())); - QObject::connect(actionOpenoC, &QAction::triggered, this, &ownCloudGui::slotOpenOwnCloud); - - FolderMan *folderMan = FolderMan::instance(); - bool firstFolder = true; - bool singleSyncFolder = folderMan->map().size() == 1 && Theme::instance()->singleSyncFolder(); - bool onePaused = false; - bool allPaused = true; - foreach (Folder *folder, folderMan->map()) { - if (folder->accountState() != accountState.data()) { - continue; - } - - if (folder->syncPaused()) { - onePaused = true; - } else { - allPaused = false; - } - - if (firstFolder && !singleSyncFolder) { - firstFolder = false; - menu->addSeparator(); - menu->addAction(tr("Managed Folders:"))->setDisabled(true); - } - - QAction *action = menu->addAction(tr("Open folder '%1'").arg(folder->shortGuiLocalPath())); - auto alias = folder->alias(); - connect(action, &QAction::triggered, this, [this, alias] { this->slotFolderOpenAction(alias); }); - } - - menu->addSeparator(); - if (separateMenu) { - if (onePaused) { - QAction *enable = menu->addAction(tr("Resume all folders")); - enable->setProperty(propertyAccountC, QVariant::fromValue(accountState)); - connect(enable, &QAction::triggered, this, &ownCloudGui::slotUnpauseAllFolders); - } - if (!allPaused) { - QAction *enable = menu->addAction(tr("Pause all folders")); - enable->setProperty(propertyAccountC, QVariant::fromValue(accountState)); - connect(enable, &QAction::triggered, this, &ownCloudGui::slotPauseAllFolders); - } - - if (accountState->isSignedOut()) { - QAction *signin = menu->addAction(tr("Log in …")); - signin->setProperty(propertyAccountC, QVariant::fromValue(accountState)); - connect(signin, &QAction::triggered, this, &ownCloudGui::slotLogin); - } else { - QAction *signout = menu->addAction(tr("Log out")); - signout->setProperty(propertyAccountC, QVariant::fromValue(accountState)); - connect(signout, &QAction::triggered, this, &ownCloudGui::slotLogout); - } - } -} - -void ownCloudGui::slotContextMenuAboutToShow() -{ - _contextMenuVisibleManual = true; - - // Update icon in sys tray, as it might change depending on the context menu state - slotComputeOverallSyncStatus(); - - if (!_workaroundNoAboutToShowUpdate) { - updateContextMenu(); - } -} - -void ownCloudGui::slotContextMenuAboutToHide() -{ - _contextMenuVisibleManual = false; - - // Update icon in sys tray, as it might change depending on the context menu state - slotComputeOverallSyncStatus(); -} - -bool ownCloudGui::contextMenuVisible() const -{ - // On some platforms isVisible doesn't work and always returns false, - // elsewhere aboutToHide is unreliable. - if (_workaroundManualVisibility) - return _contextMenuVisibleManual; - return _contextMenu->isVisible(); -} - -static bool minimalTrayMenu() -{ - static QByteArray var = qgetenv("OWNCLOUD_MINIMAL_TRAY_MENU"); - return !var.isEmpty(); -} - -static bool updateWhileVisible() -{ - static QByteArray var = qgetenv("OWNCLOUD_TRAY_UPDATE_WHILE_VISIBLE"); - if (var == "1") { - return true; - } else if (var == "0") { - return false; - } else { - // triggers bug on OS X: https://bugreports.qt.io/browse/QTBUG-54845 - // or flickering on Xubuntu - return false; - } -} - -static QByteArray envForceQDBusTrayWorkaround() -{ - static QByteArray var = qgetenv("OWNCLOUD_FORCE_QDBUS_TRAY_WORKAROUND"); - return var; -} - -static QByteArray envForceWorkaroundShowAndHideTray() -{ - static QByteArray var = qgetenv("OWNCLOUD_FORCE_TRAY_SHOW_HIDE"); - return var; -} - -static QByteArray envForceWorkaroundNoAboutToShowUpdate() -{ - static QByteArray var = qgetenv("OWNCLOUD_FORCE_TRAY_NO_ABOUT_TO_SHOW"); - return var; -} - -static QByteArray envForceWorkaroundFakeDoubleClick() -{ - static QByteArray var = qgetenv("OWNCLOUD_FORCE_TRAY_FAKE_DOUBLE_CLICK"); - return var; -} - -static QByteArray envForceWorkaroundManualVisibility() -{ - static QByteArray var = qgetenv("OWNCLOUD_FORCE_TRAY_MANUAL_VISIBILITY"); - return var; -} - -void ownCloudGui::setupContextMenu() -{ - if (_contextMenu) { - return; - } - - _contextMenu.reset(new QMenu()); - _contextMenu->setTitle(Theme::instance()->appNameGUI()); - - _recentActionsMenu = new QMenu(tr("Recent Changes"), _contextMenu.data()); - - // this must be called only once after creating the context menu, or - // it will trigger a bug in Ubuntu's SNI bridge patch (11.10, 12.04). - _tray->setContextMenu(_contextMenu.data()); - - // The tray menu is surprisingly problematic. Being able to switch to - // a minimal version of it is a useful workaround and testing tool. - if (minimalTrayMenu()) { - _contextMenu->addAction(_actionQuit); - return; - } - - auto applyEnvVariable = [](bool *sw, const QByteArray &value) { - if (value == "1") - *sw = true; - if (value == "0") - *sw = false; - }; - - // This is an old compound flag that people might still depend on - bool qdbusmenuWorkarounds = false; - applyEnvVariable(&qdbusmenuWorkarounds, envForceQDBusTrayWorkaround()); - if (qdbusmenuWorkarounds) { - _workaroundFakeDoubleClick = true; - _workaroundNoAboutToShowUpdate = true; - _workaroundShowAndHideTray = true; - } - -#ifdef Q_OS_MAC - // https://bugreports.qt.io/browse/QTBUG-54633 - _workaroundNoAboutToShowUpdate = true; - _workaroundManualVisibility = true; -#endif - -#ifdef Q_OS_LINUX - // For KDE sessions if the platform plugin is missing, - // neither aboutToShow() updates nor the isVisible() call - // work. At least aboutToHide is reliable. - // https://github.com/owncloud/client/issues/6545 - static QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP"); - static QByteArray desktopSession = qgetenv("DESKTOP_SESSION"); - bool isKde = - xdgCurrentDesktop.contains("KDE") - || desktopSession.contains("plasma") - || desktopSession.contains("kde"); - QObject *platformMenu = reinterpret_cast(_tray->contextMenu()->platformMenu()); - if (isKde && platformMenu && platformMenu->metaObject()->className() == QLatin1String("QDBusPlatformMenu")) { - _workaroundManualVisibility = true; - _workaroundNoAboutToShowUpdate = true; - } -#endif - - applyEnvVariable(&_workaroundNoAboutToShowUpdate, envForceWorkaroundNoAboutToShowUpdate()); - applyEnvVariable(&_workaroundFakeDoubleClick, envForceWorkaroundFakeDoubleClick()); - applyEnvVariable(&_workaroundShowAndHideTray, envForceWorkaroundShowAndHideTray()); - applyEnvVariable(&_workaroundManualVisibility, envForceWorkaroundManualVisibility()); - - qCInfo(lcApplication) << "Tray menu workarounds:" - << "noabouttoshow:" << _workaroundNoAboutToShowUpdate - << "fakedoubleclick:" << _workaroundFakeDoubleClick - << "showhide:" << _workaroundShowAndHideTray - << "manualvisibility:" << _workaroundManualVisibility; - - - connect(&_delayedTrayUpdateTimer, &QTimer::timeout, this, &ownCloudGui::updateContextMenu); - _delayedTrayUpdateTimer.setInterval(2 * 1000); - _delayedTrayUpdateTimer.setSingleShot(true); - - connect(_contextMenu.data(), SIGNAL(aboutToShow()), SLOT(slotContextMenuAboutToShow())); - // unfortunately aboutToHide is unreliable, it seems to work on OSX though - connect(_contextMenu.data(), SIGNAL(aboutToHide()), SLOT(slotContextMenuAboutToHide())); - - // Populate the context menu now. - updateContextMenu(); -} - -void ownCloudGui::updateContextMenu() -{ - if (minimalTrayMenu()) { - return; - } - - // If it's visible, we can't update live, and it won't be updated lazily: reschedule - if (contextMenuVisible() && !updateWhileVisible() && _workaroundNoAboutToShowUpdate) { - if (!_delayedTrayUpdateTimer.isActive()) { - _delayedTrayUpdateTimer.start(); - } - return; - } - - if (_workaroundShowAndHideTray) { - // To make tray menu updates work with these bugs (see setupContextMenu) - // we need to hide and show the tray icon. We don't want to do that - // while it's visible! - if (contextMenuVisible()) { - if (!_delayedTrayUpdateTimer.isActive()) { - _delayedTrayUpdateTimer.start(); - } - return; - } - _tray->hide(); - } - - _contextMenu->clear(); - slotRebuildRecentMenus(); - - // We must call deleteLater because we might be called from the press in one of the actions. - foreach (auto menu, _accountMenus) { - menu->deleteLater(); - } - _accountMenus.clear(); - - auto accountList = AccountManager::instance()->accounts(); - - bool isConfigured = (!accountList.isEmpty()); - bool atLeastOneConnected = false; - bool atLeastOnePaused = false; - bool atLeastOneNotPaused = false; - foreach (auto a, accountList) { - if (a->isConnected()) { - atLeastOneConnected = true; - } - } - foreach (auto f, FolderMan::instance()->map()) { - if (f->syncPaused()) { - atLeastOnePaused = true; - } else { - atLeastOneNotPaused = true; - } - } - - if (accountList.count() > 1) { - foreach (AccountStatePtr account, accountList) { - QMenu *accountMenu = new QMenu(account->account()->displayName(), _contextMenu.data()); - _accountMenus.append(accountMenu); - _contextMenu->addMenu(accountMenu); - - addAccountContextMenu(account, accountMenu, true); - fetchNavigationApps(account); - } - } else if (accountList.count() == 1) { - addAccountContextMenu(accountList.first(), _contextMenu.data(), false); - fetchNavigationApps(accountList.first()); - } - - _contextMenu->addSeparator(); - - _contextMenu->addAction(_actionStatus); - if (isConfigured && atLeastOneConnected) { - _contextMenu->addMenu(_recentActionsMenu); - } - - _contextMenu->addSeparator(); - - if (_navLinksMenu) { - _contextMenu->addMenu(_navLinksMenu); - } - - _contextMenu->addSeparator(); - - if (accountList.isEmpty()) { - _contextMenu->addAction(_actionNewAccountWizard); - } - _contextMenu->addAction(_actionSettings); - if (!Theme::instance()->helpUrl().isEmpty()) { - _contextMenu->addAction(_actionHelp); - } - - if (_actionCrash) { - _contextMenu->addAction(_actionCrash); - } - - _contextMenu->addSeparator(); - - if (atLeastOnePaused) { - QString text; - if (accountList.count() > 1) { - text = tr("Resume all synchronization"); - } else { - text = tr("Resume synchronization"); - } - QAction *action = _contextMenu->addAction(text); - connect(action, &QAction::triggered, this, &ownCloudGui::slotUnpauseAllFolders); - } - if (atLeastOneNotPaused) { - QString text; - if (accountList.count() > 1) { - text = tr("Pause all synchronization"); - } else { - text = tr("Pause synchronization"); - } - QAction *action = _contextMenu->addAction(text); - connect(action, &QAction::triggered, this, &ownCloudGui::slotPauseAllFolders); - } - _contextMenu->addAction(_actionQuit); - - if (_workaroundShowAndHideTray) { - _tray->show(); - } -} - -void ownCloudGui::updateContextMenuNeeded() -{ - // if it's visible and we can update live: update now - if (contextMenuVisible() && updateWhileVisible()) { - // Note: don't update while visible on OSX - // https://bugreports.qt.io/browse/QTBUG-54845 - updateContextMenu(); - return; - } - - // if we can't lazily update: update later - if (_workaroundNoAboutToShowUpdate) { - // Note: don't update immediately even in the invisible case - // as that can lead to extremely frequent menu updates - if (!_delayedTrayUpdateTimer.isActive()) { - _delayedTrayUpdateTimer.start(); - } - return; - } -} - void ownCloudGui::slotShowTrayMessage(const QString &title, const QString &msg) { if (_tray) @@ -747,7 +363,6 @@ void ownCloudGui::slotShowOptionalTrayMessage(const QString &title, const QStrin slotShowTrayMessage(title, msg); } - /* * open the folder with the given Alias */ @@ -771,156 +386,17 @@ void ownCloudGui::slotFolderOpenAction(const QString &alias) } } -void ownCloudGui::setupActions() -{ - _actionStatus = new QAction(tr("Unknown status"), this); - _actionStatus->setEnabled(false); - _navLinksMenu = new QMenu(tr("Apps")); - _navLinksMenu->setEnabled(false); - _actionSettings = new QAction(tr("Settings …"), this); - _actionNewAccountWizard = new QAction(tr("New account …"), this); - _actionRecent = new QAction(tr("View more activity …"), this); - _actionRecent->setEnabled(true); - - QObject::connect(_actionRecent, &QAction::triggered, this, &ownCloudGui::slotShowSyncProtocol); - QObject::connect(_actionSettings, &QAction::triggered, this, &ownCloudGui::slotShowSettings); - QObject::connect(_actionNewAccountWizard, &QAction::triggered, this, &ownCloudGui::slotNewAccountWizard); - _actionHelp = new QAction(tr("Help"), this); - QObject::connect(_actionHelp, &QAction::triggered, this, &ownCloudGui::slotHelp); - _actionQuit = new QAction(tr("Quit %1").arg(Theme::instance()->appNameGUI()), this); - QObject::connect(_actionQuit, SIGNAL(triggered(bool)), _app, SLOT(quit())); - - if (_app->debugMode()) { - _actionCrash = new QAction(tr("Crash now", "Only shows in debug mode to allow testing the crash handler"), this); - connect(_actionCrash, &QAction::triggered, _app, &Application::slotCrash); - } else { - _actionCrash = nullptr; - } -} - -void ownCloudGui::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){ - if(statusCode == 200){ - qCDebug(lcApplication) << "New navigation apps ETag Response Header received " << value; - auto account = qvariant_cast(sender()->property(propertyAccountC)); - account->setNavigationAppsEtagResponseHeader(value); - } -} - -void ownCloudGui::fetchNavigationApps(AccountStatePtr account){ - OcsNavigationAppsJob *job = new OcsNavigationAppsJob(account->account()); - job->setProperty(propertyAccountC, QVariant::fromValue(account)); - job->addRawHeader("If-None-Match", account->navigationAppsEtagResponseHeader()); - connect(job, &OcsNavigationAppsJob::appsJobFinished, this, &ownCloudGui::slotNavigationAppsFetched); - connect(job, &OcsNavigationAppsJob::etagResponseHeaderReceived, this, &ownCloudGui::slotEtagResponseHeaderReceived); - connect(job, &OcsNavigationAppsJob::ocsError, this, &ownCloudGui::slotOcsError); - job->getNavigationApps(); -} - -void ownCloudGui::buildNavigationAppsMenu(AccountStatePtr account, QMenu *accountMenu){ - auto navLinks = _navApps.value(account); - - _navLinksMenu->clear(); - _navLinksMenu->setEnabled(navLinks.size() > 0); - - if(navLinks.size() > 0){ - // when there is only one account add the nav links above the settings - QAction *actionBefore = _actionSettings; - - // when there is more than one account add the nav links above pause/unpause folder or logout action - if(AccountManager::instance()->accounts().size() > 1){ - foreach(QAction *action, accountMenu->actions()){ - - // pause/unpause folder and logout actions have propertyAccountC - if(auto actionAccount = qvariant_cast(action->property(propertyAccountC))){ - if(actionAccount == account){ - actionBefore = action; - break; - } - } - } - } - - // Create submenu with links - foreach (const QJsonValue &value, navLinks) { - auto navLink = value.toObject(); - QAction *action = new QAction(navLink.value("name").toString(), this); - QUrl href(navLink.value("href").toString()); - connect(action, &QAction::triggered, this, [href] { QDesktopServices::openUrl(href); }); - _navLinksMenu->addAction(action); - } - } -} - -void ownCloudGui::slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode) -{ - if(auto account = qvariant_cast(sender()->property(propertyAccountC))){ - if (statusCode == 304) { - qCWarning(lcApplication) << "Status code " << statusCode << " Not Modified - No new navigation apps."; - } else { - if(!reply.isEmpty()){ - auto element = reply.object().value("ocs").toObject().value("data"); - auto navLinks = element.toArray(); - _navApps.insert(account, navLinks); - } - } - - // TODO see pull #523 - auto accountList = AccountManager::instance()->accounts(); - if(accountList.size() > 1){ - // the list of apps will be displayed under the account that it belongs to - foreach (QMenu *accountMenu, _accountMenus) { - if(accountMenu->title() == account->account()->displayName()){ - buildNavigationAppsMenu(account, accountMenu); - break; - } - } - } else if(accountList.size() == 1){ - buildNavigationAppsMenu(account, _contextMenu.data()); - } - } -} - -void ownCloudGui::slotOcsError(int statusCode, const QString &message) -{ - emit serverError(statusCode, message); -} - -void ownCloudGui::slotRebuildRecentMenus() -{ - _recentActionsMenu->clear(); - if (!_recentItemsActions.isEmpty()) { - foreach (QAction *a, _recentItemsActions) { - _recentActionsMenu->addAction(a); - } - _recentActionsMenu->addSeparator(); - } else { - _recentActionsMenu->addAction(tr("No items synced recently"))->setEnabled(false); - } - // add a more... entry. - _recentActionsMenu->addAction(_actionRecent); -} - -/// Returns true if the completion of a given item should show up in the -/// 'Recent Activity' menu -static bool shouldShowInRecentsMenu(const SyncFileItem &item) -{ - return !Progress::isIgnoredKind(item._status) - && item._instruction != CSYNC_INSTRUCTION_EVAL - && item._instruction != CSYNC_INSTRUCTION_NONE; -} - - void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo &progress) { Q_UNUSED(folder); if (progress.status() == ProgressInfo::Discovery) { if (!progress._currentDiscoveredRemoteFolder.isEmpty()) { - _actionStatus->setText(tr("Checking for changes in remote '%1'") - .arg(progress._currentDiscoveredRemoteFolder)); + //_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)); + //_actionStatus->setText(tr("Checking for changes in local '%1'") + //.arg(progress._currentDiscoveredLocalFolder)); } } else if (progress.status() == ProgressInfo::Done) { QTimer::singleShot(2000, this, &ownCloudGui::slotComputeOverallSyncStatus); @@ -943,7 +419,7 @@ void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo & .arg(currentFile) .arg(totalFileCount); } - _actionStatus->setText(msg); + //_actionStatus->setText(msg); } else { QString totalSizeStr = Utility::octetsToString(progress.totalSize()); QString msg; @@ -954,18 +430,10 @@ void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo & msg = tr("Syncing %1") .arg(totalSizeStr); } - _actionStatus->setText(msg); + //_actionStatus->setText(msg); } - _actionRecent->setIcon(QIcon()); // Fixme: Set a "in-progress"-item eventually. - - if (!progress._lastCompletedItem.isEmpty() - && shouldShowInRecentsMenu(progress._lastCompletedItem)) { - if (Progress::isWarningKind(progress._lastCompletedItem._status)) { - // display a warn icon if warnings happened. - QIcon warnIcon(":/client/resources/warning"); - _actionRecent->setIcon(warnIcon); - } + if (!progress._lastCompletedItem.isEmpty()) { QString kindStr = Progress::asResultString(progress._lastCompletedItem); QString timeStr = QTime::currentTime().toString("hh:mm"); @@ -984,12 +452,6 @@ void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo & _recentItemsActions.takeFirst()->deleteLater(); } _recentItemsActions.append(action); - - // Update the "Recent" menu if the context menu is being shown, - // otherwise it'll be updated later, when the context menu is opened. - if (updateWhileVisible() && contextMenuVisible()) { - slotRebuildRecentMenus(); - } } } @@ -1097,6 +559,7 @@ void ownCloudGui::slotShutdown() _settingsDialog->close(); if (!_logBrowser.isNull()) _logBrowser->deleteLater(); + _app->quit(); } void ownCloudGui::slotToggleLogBrowser() diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h index 98af71bb792a..928a0fbd0b97 100644 --- a/src/gui/owncloudgui.h +++ b/src/gui/owncloudgui.h @@ -63,9 +63,7 @@ class ownCloudGui : public QObject void setupCloudProviders(); bool cloudProviderApiAvailable(); #endif - - /// Whether the tray menu is visible - bool contextMenuVisible() const; + void createTray(); signals: void setupProxy(); @@ -73,16 +71,10 @@ class ownCloudGui : public QObject void isShowingSettingsDialog(); public slots: - void setupContextMenu(); - void updateContextMenu(); - void updateContextMenuNeeded(); - void slotContextMenuAboutToShow(); - void slotContextMenuAboutToHide(); void slotComputeOverallSyncStatus(); void slotShowTrayMessage(const QString &title, const QString &msg); void slotShowOptionalTrayMessage(const QString &title, const QString &msg); void slotFolderOpenAction(const QString &alias); - void slotRebuildRecentMenus(); void slotUpdateProgress(const QString &folder, const ProgressInfo &progress); void slotShowGuiMessage(const QString &title, const QString &message); void slotFoldersChanged(); @@ -99,8 +91,6 @@ public slots: void slotOpenPath(const QString &path); void slotAccountStateChanged(); void slotTrayMessageIfServerUnsupported(Account *account); - void slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode); - void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode); /** @@ -114,9 +104,6 @@ public slots: void slotRemoveDestroyedShareDialogs(); -protected slots: - void slotOcsError(int statusCode, const QString &message); - private slots: void slotLogin(); void slotLogout(); @@ -126,47 +113,21 @@ private slots: private: void setPauseOnAllFoldersHelper(bool pause); - void setupActions(); - void addAccountContextMenu(AccountStatePtr accountState, QMenu *menu, bool separateMenu); - void fetchNavigationApps(AccountStatePtr account); - void buildNavigationAppsMenu(AccountStatePtr account, QMenu *accountMenu); QPointer _tray; QPointer _settingsDialog; QPointer _logBrowser; - // tray's menu - QScopedPointer _contextMenu; - - // Manually tracking whether the context menu is visible via aboutToShow - // and aboutToHide. Unfortunately aboutToHide isn't reliable everywhere - // so this only gets used with _workaroundManualVisibility (when the tray's - // isVisible() is unreliable) - bool _contextMenuVisibleManual = false; #ifdef WITH_LIBCLOUDPROVIDERS QDBusConnection _bus; #endif - QMenu *_recentActionsMenu; - QVector _accountMenus; - bool _workaroundShowAndHideTray = false; - bool _workaroundNoAboutToShowUpdate = false; - bool _workaroundFakeDoubleClick = false; - bool _workaroundManualVisibility = false; - QTimer _delayedTrayUpdateTimer; QMap> _shareDialogs; QAction *_actionNewAccountWizard; QAction *_actionSettings; - QAction *_actionStatus; QAction *_actionEstimate; - QAction *_actionRecent; - QAction *_actionHelp; - QAction *_actionQuit; - QAction *_actionCrash; - QMenu *_navLinksMenu; - QMap _navApps; QList _recentItemsActions; Application *_app; diff --git a/src/gui/servernotificationhandler.h b/src/gui/servernotificationhandler.h deleted file mode 100644 index f0276da524d1..000000000000 --- a/src/gui/servernotificationhandler.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) by Klaas Freitag - * - * 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. - */ - -#ifndef SERVERNOTIFICATIONHANDLER_H -#define SERVERNOTIFICATIONHANDLER_H - -#include - -#include "activitywidget.h" - -class QJsonDocument; - -namespace OCC { - -class ServerNotificationHandler : public QObject -{ - Q_OBJECT -public: - explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr); - static QMap iconCache; - -signals: - void newNotificationList(ActivityList); - -public slots: - void slotFetchNotifications(); - -private slots: - void slotNotificationsReceived(const QJsonDocument &json, int statusCode); - void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode); - void slotIconDownloaded(QByteArray iconData); - -private: - QPointer _notificationJob; - AccountState *_accountState; -}; -} - -#endif // SERVERNOTIFICATIONHANDLER_H diff --git a/src/gui/settingsdialog.cpp b/src/gui/settingsdialog.cpp index 0f5df7194d7b..53ff923b8768 100644 --- a/src/gui/settingsdialog.cpp +++ b/src/gui/settingsdialog.cpp @@ -23,7 +23,6 @@ #include "configfile.h" #include "progressdispatcher.h" #include "owncloudgui.h" -#include "activitywidget.h" #include "accountmanager.h" #include @@ -189,39 +188,13 @@ void SettingsDialog::showFirstPage() } } -void SettingsDialog::showActivityPage() -{ - if (auto account = qvariant_cast(sender()->property("account"))) { - _activitySettings[account]->show(); - _ui->stack->setCurrentWidget(_activitySettings[account]); - } -} - void SettingsDialog::showIssuesList(AccountState *account) { - for (auto it = _actionGroupWidgets.begin(); it != _actionGroupWidgets.end(); ++it) { + /*for (auto it = _actionGroupWidgets.begin(); it != _actionGroupWidgets.end(); ++it) { if (it.value() == _activitySettings[account]) { it.key()->activate(QAction::ActionEvent::Trigger); break; } - } -} - -void SettingsDialog::activityAdded(AccountState *s){ - _ui->stack->addWidget(_activitySettings[s]); - connect(_activitySettings[s], &ActivitySettings::guiLog, _gui, - &ownCloudGui::slotShowOptionalTrayMessage); - - ConfigFile cfg; - _activitySettings[s]->setNotificationRefreshInterval(cfg.notificationRefreshInterval()); - - // Note: all the actions have a '\n' because the account name is in two lines and - // all buttons must have the same size in order to keep a good layout - QAction *action = createColorAwareAction(QLatin1String(":/client/resources/activity.png"), tr("Activity")); - action->setProperty("account", QVariant::fromValue(s)); - _toolBar->insertAction(_actionBefore, action); - _actionGroup->addAction(action); - _actionGroupWidgets.insert(action, _activitySettings[s]); - connect(action, &QAction::triggered, this, &SettingsDialog::showActivityPage); + }*/ } void SettingsDialog::accountAdded(AccountState *s) @@ -229,14 +202,6 @@ void SettingsDialog::accountAdded(AccountState *s) auto height = _toolBar->sizeHint().height(); bool brandingSingleAccount = !Theme::instance()->multiAccount(); - _activitySettings[s] = new ActivitySettings(s, this); - - // if this is not the first account, then before we continue to add more accounts we add a separator - if(AccountManager::instance()->accounts().first().data() != s && - AccountManager::instance()->accounts().size() >= 1){ - _actionGroupWidgets.insert(_toolBar->insertSeparator(_actionBefore), _activitySettings[s]); - } - QAction *accountAction; QImage avatar = s->account()->avatar(); const QString actionText = brandingSingleAccount ? tr("Account") : s->account()->displayName(); @@ -264,19 +229,11 @@ void SettingsDialog::accountAdded(AccountState *s) connect(accountSettings, &AccountSettings::folderChanged, _gui, &ownCloudGui::slotFoldersChanged); connect(accountSettings, &AccountSettings::openFolderAlias, _gui, &ownCloudGui::slotFolderOpenAction); - connect(accountSettings, &AccountSettings::showIssuesList, this, &SettingsDialog::showIssuesList); connect(s->account().data(), &Account::accountChangedAvatar, this, &SettingsDialog::slotAccountAvatarChanged); connect(s->account().data(), &Account::accountChangedDisplayName, this, &SettingsDialog::slotAccountDisplayNameChanged); - // Refresh immediatly when getting online - connect(s, &AccountState::isConnectedChanged, this, &SettingsDialog::slotRefreshActivityAccountStateSender); - // Connect styleChanged event, to adapt (Dark-/Light-Mode switching) connect(this, &SettingsDialog::styleChanged, accountSettings, &AccountSettings::slotStyleChanged); - connect(this, &SettingsDialog::styleChanged, _activitySettings[s], &ActivitySettings::slotStyleChanged); - - activityAdded(s); - slotRefreshActivity(s); } void SettingsDialog::slotAccountAvatarChanged() @@ -332,19 +289,6 @@ void SettingsDialog::accountRemoved(AccountState *s) _actionForAccount.remove(s->account().data()); } - if(_activitySettings.contains(s)){ - _activitySettings[s]->slotRemoveAccount(); - _activitySettings[s]->hide(); - - // get the settings widget and the separator - foreach(QAction *action, _actionGroupWidgets.keys(_activitySettings[s])){ - _actionGroupWidgets.remove(action); - _toolBar->removeAction(action); - } - _toolBar->widgetForAction(_actionBefore)->hide(); - _activitySettings.remove(s); - } - // Hide when the last account is deleted. We want to enter the same // state we'd be in the client was started up without an account // configured. @@ -414,15 +358,4 @@ QAction *SettingsDialog::createColorAwareAction(const QString &iconPath, const Q return createActionWithIcon(coloredIcon, text, iconPath); } -void SettingsDialog::slotRefreshActivityAccountStateSender() -{ - slotRefreshActivity(qobject_cast(sender())); -} - -void SettingsDialog::slotRefreshActivity(AccountState *accountState) -{ - if (accountState->isConnected()) - _activitySettings[accountState]->slotRefresh(); -} - } // namespace OCC diff --git a/src/gui/settingsdialog.h b/src/gui/settingsdialog.h index f5ec654d967f..8242522c29b7 100644 --- a/src/gui/settingsdialog.h +++ b/src/gui/settingsdialog.h @@ -37,7 +37,6 @@ class AccountSettings; class Application; class FolderMan; class ownCloudGui; -class ActivitySettings; /** * @brief The SettingsDialog class @@ -55,11 +54,8 @@ class SettingsDialog : public QDialog public slots: void showFirstPage(); - void showActivityPage(); void showIssuesList(AccountState *account); void slotSwitchPage(QAction *action); - void slotRefreshActivity(AccountState *accountState); - void slotRefreshActivityAccountStateSender(); void slotAccountAvatarChanged(); void slotAccountDisplayNameChanged(); @@ -78,7 +74,6 @@ private slots: private: void customizeStyle(); - void activityAdded(AccountState *); QAction *createColorAwareAction(const QString &iconName, const QString &fileName); QAction *createActionWithIcon(const QIcon &icon, const QString &text, const QString &iconPath = QString()); @@ -95,7 +90,6 @@ private slots: QHash _actionForAccount; QToolBar *_toolBar; - QMap _activitySettings; ownCloudGui *_gui; }; diff --git a/src/gui/settingsdialog.ui b/src/gui/settingsdialog.ui index 3f886ed222bc..18632134f605 100644 --- a/src/gui/settingsdialog.ui +++ b/src/gui/settingsdialog.ui @@ -6,7 +6,7 @@ 0 0 - 693 + 516 457 diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index a380be35d6e4..be0253ddba64 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -12,9 +12,17 @@ * for more details. */ +#include "accountmanager.h" #include "systray.h" #include "theme.h" #include "config.h" +#include "tray/UserModel.h" + +#include +#include +#include +#include +#include #ifdef USE_FDO_NOTIFICATIONS #include @@ -28,10 +36,76 @@ namespace OCC { +Systray *Systray::_instance = nullptr; + +Systray *Systray::instance() +{ + if (_instance == nullptr) { + _instance = new Systray(); + } + return _instance; +} + +Systray::Systray() + : _isOpen(false) + , _syncIsPaused(false) + , _trayComponent(nullptr) + , _trayContext(nullptr) +{ + // Create QML tray engine, build component, set C++ backend context used in window.qml + // Use pointer instead of engine() helper function until Qt 5.12 is minimum standard + _trayEngine = new QQmlEngine; + _trayEngine->addImageProvider("avatars", new ImageProvider); + _trayEngine->rootContext()->setContextProperty("userModelBackend", UserModel::instance()); + _trayEngine->rootContext()->setContextProperty("appsMenuModelBackend", UserAppsModel::instance()); + _trayEngine->rootContext()->setContextProperty("systrayBackend", this); + + _trayComponent = new QQmlComponent(_trayEngine, QUrl(QStringLiteral("qrc:/qml/src/gui/tray/Window.qml"))); + + connect(UserModel::instance(), &UserModel::newUserSelected, + this, &Systray::slotNewUserSelected); + + connect(AccountManager::instance(), &AccountManager::accountAdded, + this, &Systray::showWindow); +} + +void Systray::create() +{ + if (_trayContext == nullptr) { + _trayEngine->rootContext()->setContextProperty("activityModel", UserModel::instance()->currentActivityModel()); + _trayContext = _trayEngine->contextForObject(_trayComponent->create()); + hideWindow(); + } +} + +void Systray::slotNewUserSelected() +{ + // Change ActivityModel + _trayEngine->rootContext()->setContextProperty("activityModel", UserModel::instance()->currentActivityModel()); + + // Rebuild App list + UserAppsModel::instance()->buildAppList(); +} + +bool Systray::isOpen() +{ + return _isOpen; +} + +Q_INVOKABLE void Systray::setOpened() +{ + _isOpen = true; +} + +Q_INVOKABLE void Systray::setClosed() +{ + _isOpen = false; +} + void Systray::showMessage(const QString &title, const QString &message, MessageIcon icon, int millisecondsTimeoutHint) { #ifdef USE_FDO_NOTIFICATIONS - if(QDBusInterface(NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE).isValid()) { + if (QDBusInterface(NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE).isValid()) { QList args = QList() << APPLICATION_NAME << quint32(0) << APPLICATION_ICON_NAME << title << message << QStringList() << QVariantMap() << qint32(-1); QDBusMessage method = QDBusMessage::createMethodCall(NOTIFICATIONS_SERVICE, NOTIFICATIONS_PATH, NOTIFICATIONS_IFACE, "Notify"); @@ -54,4 +128,95 @@ void Systray::setToolTip(const QString &tip) QSystemTrayIcon::setToolTip(tr("%1: %2").arg(Theme::instance()->appNameGUI(), tip)); } +int Systray::calcTrayWindowX() +{ +#ifdef Q_OS_OSX + // macOS handles DPI awareness differently + // and menu bar is always at the top, icons starting from the right + + QPoint topLeft = this->geometry().topLeft(); + QPoint topRight = this->geometry().topRight(); + int trayIconTopCenterX = (topRight - ((topRight - topLeft) * 0.5)).x(); + return trayIconTopCenterX - (400 * 0.5); +#else + QScreen *trayScreen = QGuiApplication::primaryScreen(); + int screenWidth = trayScreen->geometry().width(); + int screenHeight = trayScreen->geometry().height(); + int availableWidth = trayScreen->availableGeometry().width(); + int availableHeight = trayScreen->availableGeometry().height(); + QPoint topRightDpiAware = this->geometry().topRight() / trayScreen->devicePixelRatio(); + QPoint topLeftDpiAware = this->geometry().topLeft() / trayScreen->devicePixelRatio(); + + // get coordinates from top center point of tray icon + int trayIconTopCenterX = (topRightDpiAware - ((topRightDpiAware - topLeftDpiAware) * 0.5)).x(); + int trayIconTopCenterY = (topRightDpiAware - ((topRightDpiAware - topLeftDpiAware) * 0.5)).y(); + + if (availableHeight < screenHeight) { + // taskbar is on top or bottom + if (trayIconTopCenterX + (400 * 0.5) > availableWidth) { + return availableWidth - 400 - 12; + } else { + return trayIconTopCenterX - (400 * 0.5); + } + } else { + if (trayScreen->availableGeometry().x() > trayScreen->geometry().x()) { + // on the left + return (screenWidth - availableWidth) + 6; + } else { + // on the right + return screenWidth - 400 - (screenWidth - availableWidth) - 6; + } + } +#endif +} +int Systray::calcTrayWindowY() +{ +#ifdef Q_OS_OSX + // macOS menu bar is always 22 (effective) pixels + // don't use availableGeometry() here, because this also excludes the dock + return 22+6; +#else + QScreen *trayScreen = QGuiApplication::primaryScreen(); + int screenWidth = trayScreen->geometry().width(); + int screenHeight = trayScreen->geometry().height(); + int availableHeight = trayScreen->availableGeometry().height(); + QPoint topRightDpiAware = this->geometry().topRight() / trayScreen->devicePixelRatio(); + QPoint topLeftDpiAware = this->geometry().topLeft() / trayScreen->devicePixelRatio(); + + // get coordinates from top center point of tray icon + int trayIconTopCenterX = (topRightDpiAware - ((topRightDpiAware - topLeftDpiAware) * 0.5)).x(); + int trayIconTopCenterY = (topRightDpiAware - ((topRightDpiAware - topLeftDpiAware) * 0.5)).y(); + + if (availableHeight < screenHeight) { + // taskbar is on top or bottom + if (trayScreen->availableGeometry().y() > trayScreen->geometry().y()) { + // on top + return (screenHeight - availableHeight) + 6; + } else { + // on bottom + return screenHeight - 510 - (screenHeight - availableHeight) - 6; + } + } else { + // on the left or right + return (trayIconTopCenterY - 510 + 12); + } +#endif +} + +bool Systray::syncIsPaused() +{ + return _syncIsPaused; +} + +void Systray::pauseResumeSync() +{ + if (_syncIsPaused) { + _syncIsPaused = false; + emit resumeSync(); + } else { + _syncIsPaused = true; + emit pauseSync(); + } +} + } // namespace OCC diff --git a/src/gui/systray.h b/src/gui/systray.h index 523d4b689cad..b491b749fc28 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -16,6 +16,10 @@ #define SYSTRAY_H #include +#include + +#include "accountmanager.h" +#include "tray/UserModel.h" class QIcon; @@ -26,16 +30,56 @@ bool canOsXSendUserNotification(); void sendOsXUserNotification(const QString &title, const QString &message); #endif +namespace Ui { + class Systray; +} + /** * @brief The Systray class * @ingroup gui */ -class Systray : public QSystemTrayIcon +class Systray + : public QSystemTrayIcon { Q_OBJECT public: + static Systray *instance(); + virtual ~Systray() {}; + + void create(); void showMessage(const QString &title, const QString &message, MessageIcon icon = Information, int millisecondsTimeoutHint = 10000); void setToolTip(const QString &tip); + bool isOpen(); + + Q_INVOKABLE void pauseResumeSync(); + Q_INVOKABLE int calcTrayWindowX(); + Q_INVOKABLE int calcTrayWindowY(); + Q_INVOKABLE bool syncIsPaused(); + Q_INVOKABLE void setOpened(); + Q_INVOKABLE void setClosed(); + +signals: + void currentUserChanged(); + void openSettings(); + void openHelp(); + void shutdown(); + void pauseSync(); + void resumeSync(); + + Q_INVOKABLE void hideWindow(); + Q_INVOKABLE void showWindow(); + +public slots: + void slotNewUserSelected(); + +private: + static Systray *_instance; + Systray(); + bool _isOpen; + bool _syncIsPaused; + QQmlEngine *_trayEngine; + QQmlComponent *_trayComponent; + QQmlContext *_trayContext; }; } // namespace OCC diff --git a/src/gui/activitydata.cpp b/src/gui/tray/ActivityData.cpp similarity index 97% rename from src/gui/activitydata.cpp rename to src/gui/tray/ActivityData.cpp index 866c97956f97..9378a6596903 100644 --- a/src/gui/activitydata.cpp +++ b/src/gui/tray/ActivityData.cpp @@ -14,7 +14,7 @@ #include -#include "activitydata.h" +#include "ActivityData.h" namespace OCC { diff --git a/src/gui/activitydata.h b/src/gui/tray/ActivityData.h similarity index 97% rename from src/gui/activitydata.h rename to src/gui/tray/ActivityData.h index d2f3c864f43e..f2fc0e345676 100644 --- a/src/gui/activitydata.h +++ b/src/gui/tray/ActivityData.h @@ -56,6 +56,7 @@ class Activity Type _type; qlonglong _id; + QString _fileAction; QString _objectType; QString _subject; QString _message; @@ -64,6 +65,8 @@ class Activity QUrl _link; QDateTime _dateTime; QString _accName; + QString _icon; + QString _iconData; // Stores information about the error int _status; diff --git a/src/gui/activitylistmodel.cpp b/src/gui/tray/ActivityListModel.cpp similarity index 51% rename from src/gui/activitylistmodel.cpp rename to src/gui/tray/ActivityListModel.cpp index eeb0cc836fe2..a9b2efc64381 100644 --- a/src/gui/activitylistmodel.cpp +++ b/src/gui/tray/ActivityListModel.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -23,34 +22,44 @@ #include "accountstate.h" #include "accountmanager.h" #include "folderman.h" +#include "iconjob.h" #include "accessmanager.h" -#include "activityitemdelegate.h" -#include "activitydata.h" -#include "activitylistmodel.h" +#include "ActivityData.h" +#include "ActivityListModel.h" #include "theme.h" -#include "servernotificationhandler.h" - namespace OCC { Q_LOGGING_CATEGORY(lcActivity, "nextcloud.gui.activity", QtInfoMsg) -ActivityListModel::ActivityListModel(AccountState *accountState, QWidget *parent) - : QAbstractListModel(parent) +ActivityListModel::ActivityListModel(AccountState *accountState, QObject *parent) + : QAbstractListModel() , _accountState(accountState) { } +QHash ActivityListModel::roleNames() const +{ + QHash roles; + roles[DisplayPathRole] = "displaypath"; + roles[PathRole] = "path"; + roles[LinkRole] = "link"; + roles[MessageRole] = "message"; + roles[ActionRole] = "type"; + roles[ActionIconRole] = "icon"; + roles[ActionTextRole] = "subject"; + roles[ActionTextColorRole] = "activityTextTitleColor"; + roles[ObjectTypeRole] = "objectType"; + roles[PointInTimeRole] = "dateTime"; + return roles; +} + QVariant ActivityListModel::data(const QModelIndex &index, int role) const { Activity a; - // filter the get action here - // send only the text of the get action - // if there is more than one send the icon? the ... - if (!index.isValid()) return QVariant(); @@ -61,25 +70,44 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const QStringList list; switch (role) { - case ActivityItemDelegate::PathRole: - if(!a._file.isEmpty()){ + case DisplayPathRole: + if (!a._file.isEmpty()) { + auto folder = FolderMan::instance()->folder(a._folder); + QString relPath(a._file); + if (folder) { + relPath.prepend(folder->remotePath()); + } + list = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account()); + if (list.count() > 0) { + if (relPath.startsWith('/') || relPath.startsWith('\\')) { + return relPath.remove(0, 1); + } else { + return relPath; + } + } + } + return QString(); + case PathRole: + if (!a._file.isEmpty()) { auto folder = FolderMan::instance()->folder(a._folder); QString relPath(a._file); - if(folder) relPath.prepend(folder->remotePath()); + if (folder) + relPath.prepend(folder->remotePath()); list = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account()); if (list.count() > 0) { - return QVariant(list.at(0)); + QString path = "file:///" + QString(list.at(0)); + return QUrl(path); } // File does not exist anymore? Let's try to open its path - if(QFileInfo(relPath).exists()) { + if (QFileInfo(relPath).exists()) { list = FolderMan::instance()->findFileInLocalFolders(QFileInfo(relPath).path(), ast->account()); if (list.count() > 0) { return QVariant(list.at(0)); } } } - return QVariant(); - case ActivityItemDelegate::ActionsLinksRole:{ + return QString(); + case ActionsLinksRole: { QList customList; foreach (ActivityLink customItem, a._links) { QVariant customVariant; @@ -88,59 +116,80 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const } return customList; } - case ActivityItemDelegate::ActionIconRole:{ - ActionIcon actionIcon; - if(a._type == Activity::NotificationType){ - QIcon cachedIcon = ServerNotificationHandler::iconCache.value(a._id); - if(!cachedIcon.isNull()) { - actionIcon.iconType = ActivityIconType::iconUseCached; - actionIcon.cachedIcon = cachedIcon; + case ActionIconRole: { + if (a._type == Activity::NotificationType) { + return "qrc:///client/theme/black/bell.svg"; + } else if (a._type == Activity::SyncResultType) { + return "qrc:///client/theme/black/state-error.svg"; + } else if (a._type == Activity::SyncFileItemType) { + if (a._status == SyncFileItem::NormalError + || a._status == SyncFileItem::FatalError + || a._status == SyncFileItem::DetailError + || a._status == SyncFileItem::BlacklistedError) { + return "qrc:///client/theme/black/state-error.svg"; + } else if (a._status == SyncFileItem::SoftError + || a._status == SyncFileItem::Conflict + || a._status == SyncFileItem::Restoration + || a._status == SyncFileItem::FileLocked) { + return "qrc:///client/theme/black/state-warning.svg"; + } else if (a._status == SyncFileItem::FileIgnored) { + return "qrc:///client/theme/black/state-info.svg"; } else { - actionIcon.iconType = ActivityIconType::iconBell; + // File sync successful + if (a._fileAction == "file_created") { + return "qrc:///client/resources/add-color.svg"; + } else if (a._fileAction == "file_deleted") { + return "qrc:///client/resources/delete-color.svg"; + } else { + return "qrc:///client/resources/change.svg"; + } } - } else if(a._type == Activity::SyncResultType){ - actionIcon.iconType = ActivityIconType::iconStateError; - } else if(a._type == Activity::SyncFileItemType){ - if(a._status == SyncFileItem::NormalError - || a._status == SyncFileItem::FatalError - || a._status == SyncFileItem::DetailError - || a._status == SyncFileItem::BlacklistedError) { - actionIcon.iconType = ActivityIconType::iconStateError; - } else if(a._status == SyncFileItem::SoftError - || a._status == SyncFileItem::Conflict - || a._status == SyncFileItem::Restoration - || a._status == SyncFileItem::FileLocked){ - actionIcon.iconType = ActivityIconType::iconStateWarning; - } else if(a._status == SyncFileItem::FileIgnored){ - actionIcon.iconType = ActivityIconType::iconStateInfo; - } else { - actionIcon.iconType = ActivityIconType::iconStateSync; - } } else { - actionIcon.iconType = ActivityIconType::iconActivity; + // We have an activity + if (!a._iconData.isEmpty()) { + QString svgData = "data:image/svg+xml;utf8," + a._iconData; + return svgData; + } + return "qrc:///client/theme/black/activity.svg"; } - QVariant icn; - icn.setValue(actionIcon); - return icn; } - case ActivityItemDelegate::ObjectTypeRole: + case ObjectTypeRole: return a._objectType; - case ActivityItemDelegate::ActionRole:{ - QVariant type; - type.setValue(a._type); - return type; + case ActionRole: { + switch (a._type) { + case Activity::ActivityType: + return "Activity"; + case Activity::NotificationType: + return "Notification"; + case Activity::SyncFileItemType: + return "File"; + case Activity::SyncResultType: + return "Sync"; + default: + return QVariant(); + } } - case ActivityItemDelegate::ActionTextRole: + case ActionTextRole: return a._subject; - case ActivityItemDelegate::MessageRole: + case ActionTextColorRole: + return a._id == -1 ? QLatin1String("#808080") : QLatin1String("#222"); // FIXME: This is a temporary workaround for _showMoreActivitiesAvailableEntry + case MessageRole: + if (a._message.isEmpty()) { + return QString("No description available."); + } return a._message; - case ActivityItemDelegate::LinkRole: - return a._link; - case ActivityItemDelegate::AccountRole: + case LinkRole: { + if (a._link.isEmpty()) { + return ""; + } else { + return a._link; + } + } + case AccountRole: return a._accName; - case ActivityItemDelegate::PointInTimeRole: - return QString("%1 (%2)").arg(a._dateTime.toLocalTime().toString(Qt::DefaultLocaleShortDate), Utility::timeAgoInWords(a._dateTime.toLocalTime())); - case ActivityItemDelegate::AccountConnectedRole: + case PointInTimeRole: + return a._id == -1 ? "" : QString("%1 - %2").arg(Utility::timeAgoInWords(a._dateTime.toLocalTime()), a._dateTime.toLocalTime().toString(Qt::DefaultLocaleShortDate)); + case AccountConnectedRole: return (ast && ast->isConnected()); default: return QVariant(); @@ -171,13 +220,13 @@ void ActivityListModel::startFetchJob() if (!_accountState->isConnected()) { return; } - JsonApiJob *job = new JsonApiJob(_accountState->account(), QLatin1String("ocs/v2.php/cloud/activity"), this); + JsonApiJob *job = new JsonApiJob(_accountState->account(), QLatin1String("ocs/v2.php/apps/activity/api/v2/activity"), this); QObject::connect(job, &JsonApiJob::jsonReceived, this, &ActivityListModel::slotActivitiesReceived); QUrlQuery params; - params.addQueryItem(QLatin1String("start"), QString::number(_currentItem)); - params.addQueryItem(QLatin1String("count"), QString::number(100)); + params.addQueryItem(QLatin1String("since"), QString::number(_currentItem)); + params.addQueryItem(QLatin1String("limit"), QString::number(50)); job->addQueryParams(params); _currentlyFetching = true; @@ -200,21 +249,43 @@ void ActivityListModel::slotActivitiesReceived(const QJsonDocument &json, int st } _currentlyFetching = false; - _currentItem += activities.size(); + + QDateTime oldestDate = QDateTime::currentDateTime(); + oldestDate = oldestDate.addDays(_maxActivitiesDays * -1); foreach (auto activ, activities) { auto json = activ.toObject(); Activity a; a._type = Activity::ActivityType; + a._objectType = json.value("object_type").toString(); a._accName = ast->account()->displayName(); - a._id = json.value("id").toInt(); + a._id = json.value("activity_id").toInt(); + a._fileAction = json.value("type").toString(); a._subject = json.value("subject").toString(); a._message = json.value("message").toString(); - a._file = json.value("file").toString(); + a._file = json.value("object_name").toString(); a._link = QUrl(json.value("link").toString()); - a._dateTime = QDateTime::fromString(json.value("date").toString(), Qt::ISODate); + a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate); + a._icon = json.value("icon").toString(); + + if (!a._icon.isEmpty()) { + IconJob *iconJob = new IconJob(QUrl(a._icon)); + iconJob->setProperty("activityId", a._id); + connect(iconJob, &IconJob::jobFinished, this, &ActivityListModel::slotIconDownloaded); + } + list.append(a); + _currentItem = list.last()._id; + + _totalActivitiesFetched++; + if(_totalActivitiesFetched == _maxActivities || + a._dateTime < oldestDate) { + + _showMoreActivitiesAvailableEntry = true; + _doneFetching = true; + break; + } } _activityLists.append(list); @@ -224,76 +295,95 @@ void ActivityListModel::slotActivitiesReceived(const QJsonDocument &json, int st combineActivityLists(); } -void ActivityListModel::addErrorToActivityList(Activity activity) { +void ActivityListModel::slotIconDownloaded(QByteArray iconData) +{ + for (size_t i = 0; i < _activityLists.count(); i++) { + if (_activityLists[i]._id == sender()->property("activityId").toLongLong()) { + _activityLists[i]._iconData = iconData; + } + } +} + +void ActivityListModel::addErrorToActivityList(Activity activity) +{ qCInfo(lcActivity) << "Error successfully added to the notification list: " << activity._subject; _notificationErrorsLists.prepend(activity); combineActivityLists(); } -void ActivityListModel::addIgnoredFileToList(Activity newActivity) { +void ActivityListModel::addIgnoredFileToList(Activity newActivity) +{ qCInfo(lcActivity) << "First checking for duplicates then add file to the notification list of ignored files: " << newActivity._file; bool duplicate = false; - if(_listOfIgnoredFiles.size() == 0){ + if (_listOfIgnoredFiles.size() == 0) { _notificationIgnoredFiles = newActivity; _notificationIgnoredFiles._subject = tr("Files from the ignore list as well as symbolic links are not synced. This includes:"); _listOfIgnoredFiles.append(newActivity); return; } - foreach(Activity activity, _listOfIgnoredFiles){ - if(activity._file == newActivity._file){ + foreach (Activity activity, _listOfIgnoredFiles) { + if (activity._file == newActivity._file) { duplicate = true; break; } } - if(!duplicate){ + if (!duplicate) { _notificationIgnoredFiles._message.append(", " + newActivity._file); } } -void ActivityListModel::addNotificationToActivityList(Activity activity) { +void ActivityListModel::addNotificationToActivityList(Activity activity) +{ qCInfo(lcActivity) << "Notification successfully added to the notification list: " << activity._subject; _notificationLists.prepend(activity); combineActivityLists(); } -void ActivityListModel::clearNotifications() { +void ActivityListModel::clearNotifications() +{ qCInfo(lcActivity) << "Clear the notifications"; _notificationLists.clear(); combineActivityLists(); } -void ActivityListModel::removeActivityFromActivityList(int row) { +void ActivityListModel::removeActivityFromActivityList(int row) +{ Activity activity = _finalList.at(row); removeActivityFromActivityList(activity); combineActivityLists(); } -void ActivityListModel::addSyncFileItemToActivityList(Activity activity) { +void ActivityListModel::addSyncFileItemToActivityList(Activity activity) +{ qCInfo(lcActivity) << "Successfully added to the activity list: " << activity._subject; _syncFileItemLists.prepend(activity); combineActivityLists(); } -void ActivityListModel::removeActivityFromActivityList(Activity activity) { +void ActivityListModel::removeActivityFromActivityList(Activity activity) +{ qCInfo(lcActivity) << "Activity/Notification/Error successfully dismissed: " << activity._subject; qCInfo(lcActivity) << "Trying to remove Activity/Notification/Error from view... "; int index = -1; - if(activity._type == Activity::ActivityType){ + if (activity._type == Activity::ActivityType) { index = _activityLists.indexOf(activity); - if(index != -1) _activityLists.removeAt(index); - } else if(activity._type == Activity::NotificationType){ + if (index != -1) + _activityLists.removeAt(index); + } else if (activity._type == Activity::NotificationType) { index = _notificationLists.indexOf(activity); - if(index != -1) _notificationLists.removeAt(index); + if (index != -1) + _notificationLists.removeAt(index); } else { index = _notificationErrorsLists.indexOf(activity); - if(index != -1) _notificationErrorsLists.removeAt(index); + if (index != -1) + _notificationErrorsLists.removeAt(index); } - if(index != -1){ + if (index != -1) { qCInfo(lcActivity) << "Activity/Notification/Error successfully removed from the list."; qCInfo(lcActivity) << "Updating Activity/Notification/Error view."; combineActivityLists(); @@ -304,38 +394,57 @@ void ActivityListModel::combineActivityLists() { ActivityList resultList; - if(_notificationErrorsLists.count() > 0) { + if (_notificationErrorsLists.count() > 0) { std::sort(_notificationErrorsLists.begin(), _notificationErrorsLists.end()); resultList.append(_notificationErrorsLists); } - if(_listOfIgnoredFiles.size() > 0) + if (_listOfIgnoredFiles.size() > 0) resultList.append(_notificationIgnoredFiles); - if(_notificationLists.count() > 0) { + if (_notificationLists.count() > 0) { std::sort(_notificationLists.begin(), _notificationLists.end()); resultList.append(_notificationLists); } - if(_syncFileItemLists.count() > 0) { + if (_syncFileItemLists.count() > 0) { std::sort(_syncFileItemLists.begin(), _syncFileItemLists.end()); resultList.append(_syncFileItemLists); } - if(_activityLists.count() > 0) { + if (_activityLists.count() > 0) { std::sort(_activityLists.begin(), _activityLists.end()); resultList.append(_activityLists); + + if(_showMoreActivitiesAvailableEntry) { + Activity a; + a._type = Activity::ActivityType; + a._accName = _accountState->account()->displayName(); + a._id = -1; + a._subject = tr("For more activities please open the Activity app."); + a._dateTime = QDateTime::currentDateTime(); + + AccountApp *app = _accountState->findApp(QLatin1String("activity")); + if(app) { + a._link = app->url(); + } + + resultList.append(a); + } } beginResetModel(); _finalList.clear(); endResetModel(); - beginInsertRows(QModelIndex(), 0, resultList.count()); - _finalList = resultList; - endInsertRows(); + if (resultList.count() > 0) { + beginInsertRows(QModelIndex(), 0, resultList.count() - 1); + _finalList = resultList; + endInsertRows(); + } } -bool ActivityListModel::canFetchActivities() const { +bool ActivityListModel::canFetchActivities() const +{ return _accountState->isConnected() && _accountState->account()->capabilities().hasActivities(); } @@ -354,6 +463,8 @@ void ActivityListModel::slotRefreshActivity() _activityLists.clear(); _doneFetching = false; _currentItem = 0; + _totalActivitiesFetched = 0; + _showMoreActivitiesAvailableEntry = false; if (canFetchActivities()) { startFetchJob(); @@ -370,5 +481,7 @@ void ActivityListModel::slotRemoveAccount() _currentlyFetching = false; _doneFetching = false; _currentItem = 0; + _totalActivitiesFetched = 0; + _showMoreActivitiesAvailableEntry = false; } } diff --git a/src/gui/activitylistmodel.h b/src/gui/tray/ActivityListModel.h similarity index 77% rename from src/gui/activitylistmodel.h rename to src/gui/tray/ActivityListModel.h index 1ff352beb331..37a766907fb2 100644 --- a/src/gui/activitylistmodel.h +++ b/src/gui/tray/ActivityListModel.h @@ -17,7 +17,7 @@ #include -#include "activitydata.h" +#include "ActivityData.h" class QJsonDocument; @@ -38,21 +38,24 @@ class ActivityListModel : public QAbstractListModel { Q_OBJECT public: - enum ActivityIconType { - iconUseCached = 0, - iconActivity, - iconBell, - iconStateError, - iconStateWarning, - iconStateInfo, - iconStateSync - }; - struct ActionIcon { - ActivityIconType iconType; - QIcon cachedIcon; - }; - - explicit ActivityListModel(AccountState *accountState, QWidget *parent = nullptr); + enum DataRole { + ActionIconRole = Qt::UserRole + 1, + UserIconRole, + AccountRole, + ObjectTypeRole, + ActionsLinksRole, + ActionTextRole, + ActionTextColorRole, + ActionRole, + MessageRole, + DisplayPathRole, + PathRole, + LinkRole, + PointInTimeRole, + AccountConnectedRole, + SyncFileStatusRole}; + + explicit ActivityListModel(AccountState *accountState, QObject* parent = 0); QVariant data(const QModelIndex &index, int role) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; @@ -76,10 +79,14 @@ public slots: private slots: void slotActivitiesReceived(const QJsonDocument &json, int statusCode); + void slotIconDownloaded(QByteArray iconData); signals: void activityJobStatusCode(int statusCode); +protected: + QHash roleNames() const override; + private: void startFetchJob(); void combineActivityLists(); @@ -96,9 +103,12 @@ private slots: bool _currentlyFetching = false; bool _doneFetching = false; int _currentItem = 0; + + int _totalActivitiesFetched = 0; + int _maxActivities = 100; + int _maxActivitiesDays = 30; + bool _showMoreActivitiesAvailableEntry = false; }; } -Q_DECLARE_METATYPE(OCC::ActivityListModel::ActionIcon) - #endif // ACTIVITYLISTMODEL_H diff --git a/src/gui/servernotificationhandler.cpp b/src/gui/tray/NotificationHandler.cpp similarity index 77% rename from src/gui/servernotificationhandler.cpp rename to src/gui/tray/NotificationHandler.cpp index bcf38603357d..4571f9a9dfe1 100644 --- a/src/gui/servernotificationhandler.cpp +++ b/src/gui/tray/NotificationHandler.cpp @@ -1,18 +1,5 @@ -/* - * Copyright (C) by Klaas Freitag - * - * 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 "servernotificationhandler.h" +#include "NotificationHandler.h" + #include "accountstate.h" #include "capabilities.h" #include "networkjobs.h" @@ -30,7 +17,7 @@ const QString notificationsPath = QLatin1String("ocs/v2.php/apps/notifications/a const char propertyAccountStateC[] = "oc_account_state"; const int successStatusCode = 200; const int notModifiedStatusCode = 304; -QMap ServerNotificationHandler::iconCache; +QMap ServerNotificationHandler::iconCache; ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState, QObject *parent) : QObject(parent) @@ -41,9 +28,7 @@ ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState, void ServerNotificationHandler::slotFetchNotifications() { // check connectivity and credentials - if (!(_accountState && _accountState->isConnected() && - _accountState->account() && _accountState->account()->credentials() && - _accountState->account()->credentials()->ready())) { + if (!(_accountState && _accountState->isConnected() && _accountState->account() && _accountState->account()->credentials() && _accountState->account()->credentials()->ready())) { deleteLater(); return; } @@ -68,18 +53,18 @@ void ServerNotificationHandler::slotFetchNotifications() _notificationJob->start(); } -void ServerNotificationHandler::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){ - if(statusCode == successStatusCode){ +void ServerNotificationHandler::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode) +{ + if (statusCode == successStatusCode) { qCWarning(lcServerNotification) << "New Notification ETag Response Header received " << value; AccountState *account = qvariant_cast(sender()->property(propertyAccountStateC)); account->setNotificationsEtagResponseHeader(value); } } -void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData){ - QPixmap pixmap; - pixmap.loadFromData(iconData); - iconCache.insert(sender()->property("activityId").toInt(), QIcon(pixmap)); +void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData) +{ + iconCache.insert(sender()->property("activityId").toInt(),iconData); } void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &json, int statusCode) @@ -107,7 +92,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j auto json = element.toObject(); a._type = Activity::NotificationType; a._accName = ai->account()->displayName(); - a._id = json.value("notification_id").toInt(); + a._id = json.value("activity_id").toInt(); //need to know, specially for remote_share a._objectType = json.value("object_type").toString(); @@ -115,16 +100,17 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j a._subject = json.value("subject").toString(); a._message = json.value("message").toString(); + a._icon = json.value("icon").toString(); - if(!json.value("icon").toString().isEmpty()){ - IconJob *iconJob = new IconJob(QUrl(json.value("icon").toString())); + if (!a._icon.isEmpty()) { + IconJob *iconJob = new IconJob(QUrl(a._icon)); iconJob->setProperty("activityId", a._id); connect(iconJob, &IconJob::jobFinished, this, &ServerNotificationHandler::slotIconDownloaded); } QUrl link(json.value("link").toString()); if (!link.isEmpty()) { - if(link.host().isEmpty()){ + if (link.host().isEmpty()) { link.setScheme(ai->account()->url().scheme()); link.setHost(ai->account()->url().host()); } @@ -151,8 +137,8 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j // https://github.com/owncloud/notifications/blob/master/docs/ocs-endpoint-v1.md#deleting-a-notification-for-a-user ActivityLink al; al._label = tr("Dismiss"); - al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString(); - al._verb = "DELETE"; + al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString(); + al._verb = "DELETE"; al._isPrimary = false; a._links.append(al); @@ -162,4 +148,4 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j deleteLater(); } -} +} \ No newline at end of file diff --git a/src/gui/tray/NotificationHandler.h b/src/gui/tray/NotificationHandler.h new file mode 100644 index 000000000000..69e286e783e3 --- /dev/null +++ b/src/gui/tray/NotificationHandler.h @@ -0,0 +1,36 @@ +#ifndef NOTIFICATIONHANDLER_H +#define NOTIFICATIONHANDLER_H + +#include + +#include "UserModel.h" + +class QJsonDocument; + +namespace OCC { + +class ServerNotificationHandler : public QObject +{ + Q_OBJECT +public: + explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr); + static QMap iconCache; + +signals: + void newNotificationList(ActivityList); + +public slots: + void slotFetchNotifications(); + +private slots: + void slotNotificationsReceived(const QJsonDocument &json, int statusCode); + void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode); + void slotIconDownloaded(QByteArray iconData); + +private: + QPointer _notificationJob; + AccountState *_accountState; +}; +} + +#endif // NOTIFICATIONHANDLER_H \ No newline at end of file diff --git a/src/gui/tray/UserLine.qml b/src/gui/tray/UserLine.qml new file mode 100644 index 000000000000..357be6005246 --- /dev/null +++ b/src/gui/tray/UserLine.qml @@ -0,0 +1,156 @@ +import QtQuick 2.9 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.2 + +MenuItem { + id: userLine + height: 60 + + RowLayout { + id: userLineLayout + spacing: 0 + width: 220 + height: 60 + + Button { + id: accountButton + Layout.preferredWidth: (userLineLayout.width * (5/6)) + Layout.preferredHeight: (userLineLayout.height) + display: AbstractButton.IconOnly + flat: true + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onContainsMouseChanged: { + accountStateIndicatorBackground.color = (containsMouse ? "#f6f6f6" : "white") + } + onClicked: { + if (!isCurrentUser) { + userModelBackend.switchCurrentUser(id) + } else { + accountMenu.close() + } + } + } + + background: Rectangle { + color: "transparent" + } + + RowLayout { + id: accountControlRowLayout + height: accountButton.height + width: accountButton.width + spacing: 0 + Image { + id: accountAvatar + Layout.leftMargin: 4 + verticalAlignment: Qt.AlignCenter + cache: false + source: ("image://avatars/" + id) + Layout.preferredHeight: (userLineLayout.height -16) + Layout.preferredWidth: (userLineLayout.height -16) + Rectangle { + id: accountStateIndicatorBackground + width: accountStateIndicator.sourceSize.width + 2 + height: width + anchors.bottom: accountAvatar.bottom + anchors.right: accountAvatar.right + color: "white" + radius: width*0.5 + } + Image { + id: accountStateIndicator + source: isConnected ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg" + cache: false + x: accountStateIndicatorBackground.x + 1 + y: accountStateIndicatorBackground.y + 1 + sourceSize.width: 16 + sourceSize.height: 16 + } + } + + Column { + id: accountLabels + spacing: 4 + Layout.alignment: Qt.AlignLeft + Layout.leftMargin: 6 + Label { + id: accountUser + width: 128 + text: name + elide: Text.ElideRight + color: "black" + font.pixelSize: 12 + font.bold: true + } + Label { + id: accountServer + width: 128 + text: server + elide: Text.ElideRight + color: "black" + font.pixelSize: 10 + } + } + } + } // accountButton + + Button { + id: userMoreButton + Layout.preferredWidth: (userLineLayout.width * (1/6)) + Layout.preferredHeight: userLineLayout.height + flat: true + + icon.source: "qrc:///client/resources/more.svg" + icon.color: "transparent" + + MouseArea { + id: userMoreButtonMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: + { + userMoreButtonMenu.popup() + } + } + background: + Rectangle { + color: userMoreButtonMouseArea.containsMouse ? "grey" : "transparent" + opacity: 0.2 + height: userMoreButton.height - 2 + y: userMoreButton.y + 1 + } + + Menu { + id: userMoreButtonMenu + width: 120 + + background: Rectangle { + border.color: "#0082c9" + radius: 2 + } + + MenuItem { + text: isConnected ? qsTr("Log out") : qsTr("Log in") + font.pixelSize: 12 + onClicked: { + isConnected ? userModelBackend.logout(index) : userModelBackend.login(index) + accountMenu.close() + } + } + + MenuItem { + text: qsTr("Remove Account") + font.pixelSize: 12 + onClicked: { + userModelBackend.removeAccount(index) + accountMenu.close() + } + } + } + } + } +} // MenuItem userLine diff --git a/src/gui/tray/UserModel.cpp b/src/gui/tray/UserModel.cpp new file mode 100644 index 000000000000..2c66894e63ef --- /dev/null +++ b/src/gui/tray/UserModel.cpp @@ -0,0 +1,868 @@ +#include "NotificationHandler.h" +#include "UserModel.h" + +#include "accountmanager.h" +#include "owncloudgui.h" +#include "syncengine.h" +#include "ocsjob.h" +#include "configfile.h" +#include "notificationconfirmjob.h" + +#include +#include +#include +#include +#include +#include + +// time span in milliseconds which has to be between two +// refreshes of the notifications +#define NOTIFICATION_REQUEST_FREE_PERIOD 15000 + +namespace OCC { + +User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent) + : QObject(parent) + , _account(account) + , _isCurrentUser(isCurrent) + , _activityModel(new ActivityListModel(_account.data())) + , _notificationRequestsRunning(0) +{ + connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo, + this, &User::slotProgressInfo); + connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted, + this, &User::slotItemCompleted); + connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError, + this, &User::slotAddError); + + connect(&_notificationCheckTimer, &QTimer::timeout, + this, &User::slotRefresh); + + connect(_account.data(), &AccountState::stateChanged, + [=]() { if (isConnected()) {slotRefresh();} }); + connect(_account.data(), &AccountState::hasFetchedNavigationApps, + this, &User::slotRebuildNavigationAppList); +} + +void User::slotBuildNotificationDisplay(const ActivityList &list) +{ + // Whether a new notification was added to the list + bool newNotificationShown = false; + + _activityModel->clearNotifications(); + + foreach (auto activity, list) { + if (_blacklistedNotifications.contains(activity)) { + qCInfo(lcActivity) << "Activity in blacklist, skip"; + continue; + } + + // handle gui logs. In order to NOT annoy the user with every fetching of the + // notifications the notification id is stored in a Set. Only if an id + // is not in the set, it qualifies for guiLog. + // Important: The _guiLoggedNotifications set must be wiped regularly which + // will repeat the gui log. + + // after one hour, clear the gui log notification store + if (_guiLogTimer.elapsed() > 60 * 60 * 1000) { + _guiLoggedNotifications.clear(); + } + + if (!_guiLoggedNotifications.contains(activity._id)) { + newNotificationShown = true; + _guiLoggedNotifications.insert(activity._id); + + // Assemble a tray notification for the NEW notification + ConfigFile cfg; + if (cfg.optionalServerNotifications()) { + if (AccountManager::instance()->accounts().count() == 1) { + emit guiLog(activity._subject, ""); + } else { + emit guiLog(activity._subject, activity._accName); + } + } + } + + _activityModel->addNotificationToActivityList(activity); + } + + // restart the gui log timer now that we show a new notification + if (newNotificationShown) { + _guiLogTimer.start(); + } +} + +void User::setNotificationRefreshInterval(std::chrono::milliseconds interval) +{ + qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval"; + _notificationCheckTimer.start(interval.count()); +} + +void User::slotRefreshImmediately() { + if (_account.data() && _account.data()->isConnected()) { + this->slotRefreshActivities(); + } + this->slotRefreshNotifications(); +} + +void User::slotRefresh() +{ + // QElapsedTimer isn't actually constructed as invalid. + if (!_timeSinceLastCheck.contains(_account.data())) { + _timeSinceLastCheck[_account.data()].invalidate(); + } + QElapsedTimer &timer = _timeSinceLastCheck[_account.data()]; + + // Fetch Activities only if visible and if last check is longer than 15 secs ago + if (timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD) { + qCDebug(lcActivity) << "Do not check as last check is only secs ago: " << timer.elapsed() / 1000; + return; + } + if (_account.data() && _account.data()->isConnected()) { + if (!timer.isValid()) { + this->slotRefreshActivities(); + } + this->slotRefreshNotifications(); + timer.start(); + } +} + +void User::slotRefreshActivities() +{ + _activityModel->slotRefreshActivity(); +} + +void User::slotRefreshNotifications() +{ + // start a server notification handler if no notification requests + // are running + if (_notificationRequestsRunning == 0) { + ServerNotificationHandler *snh = new ServerNotificationHandler(_account.data()); + connect(snh, &ServerNotificationHandler::newNotificationList, + this, &User::slotBuildNotificationDisplay); + + snh->slotFetchNotifications(); + } else { + qCWarning(lcActivity) << "Notification request counter not zero."; + } +} + +void User::slotRebuildNavigationAppList() +{ + // Rebuild App list + UserAppsModel::instance()->buildAppList(); +} + +void User::slotNotificationRequestFinished(int statusCode) +{ + int row = sender()->property("activityRow").toInt(); + + // the ocs API returns stat code 100 or 200 inside the xml if it succeeded. + if (statusCode != OCS_SUCCESS_STATUS_CODE && statusCode != OCS_SUCCESS_STATUS_CODE_V2) { + qCWarning(lcActivity) << "Notification Request to Server failed, leave notification visible."; + } else { + // to do use the model to rebuild the list or remove the item + qCWarning(lcActivity) << "Notification Request to Server successed, rebuilding list."; + _activityModel->removeActivityFromActivityList(row); + } +} + +void User::slotEndNotificationRequest(int replyCode) +{ + _notificationRequestsRunning--; + slotNotificationRequestFinished(replyCode); +} + +void User::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row) +{ + qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName; + + const QStringList validVerbs = QStringList() << "GET" + << "PUT" + << "POST" + << "DELETE"; + + if (validVerbs.contains(verb)) { + AccountStatePtr acc = AccountManager::instance()->account(accountName); + if (acc) { + NotificationConfirmJob *job = new NotificationConfirmJob(acc->account()); + QUrl l(link); + job->setLinkAndVerb(l, verb); + job->setProperty("activityRow", QVariant::fromValue(row)); + connect(job, &AbstractNetworkJob::networkError, + this, &User::slotNotifyNetworkError); + connect(job, &NotificationConfirmJob::jobFinished, + this, &User::slotNotifyServerFinished); + job->start(); + + // count the number of running notification requests. If this member var + // is larger than zero, no new fetching of notifications is started + _notificationRequestsRunning++; + } + } else { + qCWarning(lcActivity) << "Notification Links: Invalid verb:" << verb; + } +} + +void User::slotNotifyNetworkError(QNetworkReply *reply) +{ + NotificationConfirmJob *job = qobject_cast(sender()); + if (!job) { + return; + } + + int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + slotEndNotificationRequest(resultCode); + qCWarning(lcActivity) << "Server notify job failed with code " << resultCode; +} + +void User::slotNotifyServerFinished(const QString &reply, int replyCode) +{ + NotificationConfirmJob *job = qobject_cast(sender()); + if (!job) { + return; + } + + slotEndNotificationRequest(replyCode); + qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply; +} + +void User::slotProgressInfo(const QString &folder, const ProgressInfo &progress) +{ + if (progress.status() == ProgressInfo::Reconcile) { + // Wipe all non-persistent entries - as well as the persistent ones + // in cases where a local discovery was done. + auto f = FolderMan::instance()->folder(folder); + if (!f) + return; + const auto &engine = f->syncEngine(); + const auto style = engine.lastLocalDiscoveryStyle(); + foreach (Activity activity, _activityModel->errorsList()) { + if (activity._folder != folder) { + continue; + } + + if (style == LocalDiscoveryStyle::FilesystemOnly) { + _activityModel->removeActivityFromActivityList(activity); + continue; + } + + if (activity._status == SyncFileItem::Conflict && !QFileInfo(f->path() + activity._file).exists()) { + _activityModel->removeActivityFromActivityList(activity); + continue; + } + + if (activity._status == SyncFileItem::FileLocked && !QFileInfo(f->path() + activity._file).exists()) { + _activityModel->removeActivityFromActivityList(activity); + continue; + } + + + if (activity._status == SyncFileItem::FileIgnored && !QFileInfo(f->path() + activity._file).exists()) { + _activityModel->removeActivityFromActivityList(activity); + continue; + } + + + if (!QFileInfo(f->path() + activity._file).exists()) { + _activityModel->removeActivityFromActivityList(activity); + continue; + } + + auto path = QFileInfo(activity._file).dir().path().toUtf8(); + if (path == ".") + path.clear(); + + if (engine.shouldDiscoverLocally(path)) + _activityModel->removeActivityFromActivityList(activity); + } + } + + if (progress.status() == ProgressInfo::Done) { + // We keep track very well of pending conflicts. + // Inform other components about them. + QStringList conflicts; + foreach (Activity activity, _activityModel->errorsList()) { + if (activity._folder == folder + && activity._status == SyncFileItem::Conflict) { + conflicts.append(activity._file); + } + } + + emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts); + } +} + +void User::slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category) +{ + auto folderInstance = FolderMan::instance()->folder(folderAlias); + if (!folderInstance) + return; + + if (folderInstance->accountState() == _account.data()) { + qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << message; + + Activity activity; + activity._type = Activity::SyncResultType; + activity._status = SyncResult::Error; + activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate); + activity._subject = message; + activity._message = folderInstance->shortGuiLocalPath(); + activity._link = folderInstance->shortGuiLocalPath(); + activity._accName = folderInstance->accountState()->account()->displayName(); + activity._folder = folderAlias; + + + if (category == ErrorCategory::InsufficientRemoteStorage) { + ActivityLink link; + link._label = tr("Retry all uploads"); + link._link = folderInstance->path(); + link._verb = ""; + link._isPrimary = true; + activity._links.append(link); + } + + // add 'other errors' to activity list + _activityModel->addErrorToActivityList(activity); + } +} + +void User::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item) +{ + auto folderInstance = FolderMan::instance()->folder(folder); + + if (!folderInstance) + return; + + // check if we are adding it to the right account and if it is useful information (protocol errors) + if (folderInstance->accountState() == _account.data()) { + qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in " << item->_errorString; + + Activity activity; + activity._type = Activity::SyncFileItemType; //client activity + activity._status = item->_status; + activity._dateTime = QDateTime::currentDateTime(); + activity._message = item->_originalFile; + activity._link = folderInstance->accountState()->account()->url(); + activity._accName = folderInstance->accountState()->account()->displayName(); + activity._file = item->_file; + activity._folder = folder; + activity._fileAction = ""; + + if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) { + activity._fileAction = "file_deleted"; + } else if (item->_instruction == CSYNC_INSTRUCTION_NEW) { + activity._fileAction = "file_created"; + } else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) { + activity._fileAction = "file_renamed"; + } else { + activity._fileAction = "file_changed"; + } + + + if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) { + qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully."; + + if (activity._fileAction == "file_renamed") { + activity._message.prepend(tr("You renamed") + " "); + } else if (activity._fileAction == "file_deleted") { + activity._message.prepend(tr("You deleted") + " "); + } else if (activity._fileAction == "file_created") { + activity._message.prepend(tr("You created") + " "); + } else { + activity._message.prepend(tr("You changed") + " "); + } + + _activityModel->addSyncFileItemToActivityList(activity); + } else { + qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString; + activity._subject = item->_errorString; + + if (item->_status == SyncFileItem::Status::FileIgnored) { + _activityModel->addIgnoredFileToList(activity); + } else { + // add 'protocol error' to activity list + _activityModel->addErrorToActivityList(activity); + } + } + } +} + +AccountPtr User::account() const +{ + return _account->account(); +} + +void User::setCurrentUser(const bool &isCurrent) +{ + _isCurrentUser = isCurrent; +} + +Folder *User::getFolder() +{ + foreach (Folder *folder, FolderMan::instance()->map()) { + if (folder->accountState() == _account.data()) { + return folder; + } + } +} + +ActivityListModel *User::getActivityModel() +{ + return _activityModel; +} + +void User::openLocalFolder() +{ +#ifdef Q_OS_WIN + QString path = "file:///" + this->getFolder()->path(); +#else + QString path = "file://" + this->getFolder()->path(); +#endif + QDesktopServices::openUrl(path); +} + +void User::login() const +{ + _account->account()->resetRejectedCertificates(); + _account->signIn(); +} + +void User::logout() const +{ + _account->signOutByUi(); +} + +QString User::name() const +{ + // If davDisplayName is empty (can be several reasons, simplest is missing login at startup), fall back to username + QString name = _account->account()->davDisplayName(); + if (name == "") { + name = _account->account()->credentials()->user(); + } + return name; +} + +QString User::server(bool shortened) const +{ + QString serverUrl = _account->account()->url().toString(); + if (shortened) { + serverUrl.replace(QLatin1String("https://"), QLatin1String("")); + serverUrl.replace(QLatin1String("http://"), QLatin1String("")); + } + return serverUrl; +} + +QImage User::avatar(bool whiteBg) const +{ + QImage img = AvatarJob::makeCircularAvatar(_account->account()->avatar()); + if (img.isNull()) { + QImage image(128, 128, QImage::Format_ARGB32); + image.fill(Qt::GlobalColor::transparent); + QPainter painter(&image); + + QSvgRenderer renderer(QString(whiteBg ? ":/client/theme/black/user.svg" : ":/client/theme/white/user.svg")); + renderer.render(&painter); + + return image; + } else { + return img; + } +} + +bool User::serverHasTalk() const +{ + return _account->hasTalk(); +} + +bool User::hasActivities() const +{ + return _account->account()->capabilities().hasActivities(); +} + +AccountAppList User::appList() const +{ + return _account->appList(); +} + +bool User::isCurrentUser() const +{ + return _isCurrentUser; +} + +bool User::isConnected() const +{ + return (_account->connectionStatus() == AccountState::ConnectionStatus::Connected); +} + +void User::removeAccount() const +{ + AccountManager::instance()->deleteAccount(_account.data()); + AccountManager::instance()->save(); +} + +/*-------------------------------------------------------------------------------------*/ + +UserModel *UserModel::_instance = nullptr; + +UserModel *UserModel::instance() +{ + if (_instance == nullptr) { + _instance = new UserModel(); + } + return _instance; +} + +UserModel::UserModel(QObject *parent) + : QAbstractListModel(parent) + , _currentUserId() +{ + // TODO: Remember selected user from last quit via settings file + if (AccountManager::instance()->accounts().size() > 0) { + buildUserList(); + } + + connect(AccountManager::instance(), &AccountManager::accountAdded, + this, &UserModel::buildUserList); +} + +void UserModel::buildUserList() +{ + for (int i = 0; i < AccountManager::instance()->accounts().size(); i++) { + auto user = AccountManager::instance()->accounts().at(i); + addUser(user); + } + if (_init) { + _users.first()->setCurrentUser(true); + _init = false; + } +} + +Q_INVOKABLE int UserModel::numUsers() +{ + return _users.size(); +} + +Q_INVOKABLE int UserModel::currentUserId() +{ + return _currentUserId; +} + +Q_INVOKABLE bool UserModel::isUserConnected(const int &id) +{ + return _users[id]->isConnected(); +} + +Q_INVOKABLE QImage UserModel::currentUserAvatar() +{ + if (_users.count() >= 1) { + return _users[_currentUserId]->avatar(); + } else { + QImage image(128, 128, QImage::Format_ARGB32); + image.fill(Qt::GlobalColor::transparent); + QPainter painter(&image); + QSvgRenderer renderer(QString(":/client/theme/white/user.svg")); + renderer.render(&painter); + + return image; + } +} + +QImage UserModel::avatarById(const int &id) +{ + return _users[id]->avatar(true); +} + +Q_INVOKABLE QString UserModel::currentUserName() +{ + if (_users.count() >= 1) { + return _users[_currentUserId]->name(); + } else { + return QString("No users"); + } +} + +Q_INVOKABLE QString UserModel::currentUserServer() +{ + if (_users.count() >= 1) { + return _users[_currentUserId]->server(); + } else { + return QString(""); + } +} + +Q_INVOKABLE bool UserModel::currentServerHasTalk() +{ + if (_users.count() >= 1) { + return _users[_currentUserId]->serverHasTalk(); + } else { + return false; + } +} + +void UserModel::addUser(AccountStatePtr &user, const bool &isCurrent) +{ + bool containsUser = false; + for (int i = 0; i < _users.size(); i++) { + if (_users[i]->account() == user->account()) { + containsUser = true; + continue; + } + } + + if (!containsUser) { + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + _users << new User(user, isCurrent); + if (isCurrent) { + _currentUserId = _users.indexOf(_users.last()); + } + endInsertRows(); + ConfigFile cfg; + _users.last()->setNotificationRefreshInterval(cfg.notificationRefreshInterval()); + } +} + +int UserModel::currentUserIndex() +{ + return _currentUserId; +} + +Q_INVOKABLE void UserModel::openCurrentAccountLocalFolder() +{ + _users[_currentUserId]->openLocalFolder(); +} + +Q_INVOKABLE void UserModel::openCurrentAccountTalk() +{ + QString url = _users[_currentUserId]->server(false) + "/apps/spreed"; + if (!(url.contains("http://") || url.contains("https://"))) { + url = "https://" + _users[_currentUserId]->server(false) + "/apps/spreed"; + } + QDesktopServices::openUrl(QUrl(url)); +} + +Q_INVOKABLE void UserModel::openCurrentAccountServer() +{ + // Don't open this URL when the QML appMenu pops up on click (see Window.qml) + if(appList().count() > 0) + return; + + QString url = _users[_currentUserId]->server(false); + if (!(url.contains("http://") || url.contains("https://"))) { + url = "https://" + _users[_currentUserId]->server(false); + } + QDesktopServices::openUrl(QUrl(url)); +} + +Q_INVOKABLE void UserModel::switchCurrentUser(const int &id) +{ + _users[_currentUserId]->setCurrentUser(false); + _users[id]->setCurrentUser(true); + _currentUserId = id; + emit refreshCurrentUserGui(); + emit newUserSelected(); +} + +Q_INVOKABLE void UserModel::login(const int &id) +{ + _users[id]->login(); + emit refreshCurrentUserGui(); +} + +Q_INVOKABLE void UserModel::logout(const int &id) +{ + _users[id]->logout(); + emit refreshCurrentUserGui(); +} + +Q_INVOKABLE void UserModel::removeAccount(const int &id) +{ + QMessageBox messageBox(QMessageBox::Question, + tr("Confirm Account Removal"), + tr("

Do you really want to remove the connection to the account %1?

" + "

Note: This will not delete any files.

") + .arg(_users[id]->name()), + QMessageBox::NoButton); + QPushButton *yesButton = + messageBox.addButton(tr("Remove connection"), QMessageBox::YesRole); + messageBox.addButton(tr("Cancel"), QMessageBox::NoRole); + + messageBox.exec(); + if (messageBox.clickedButton() != yesButton) { + return; + } + + if (_users[id]->isCurrentUser() && _users.count() > 1) { + id == 0 ? switchCurrentUser(1) : switchCurrentUser(0); + } + + _users[id]->logout(); + _users[id]->removeAccount(); + + beginRemoveRows(QModelIndex(), id, id); + _users.removeAt(id); + endRemoveRows(); + + emit refreshCurrentUserGui(); +} + +int UserModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return _users.count(); +} + +QVariant UserModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= _users.count()) { + return QVariant(); + } + + if (role == NameRole) { + return _users[index.row()]->name(); + } else if (role == ServerRole) { + return _users[index.row()]->server(); + } else if (role == AvatarRole) { + return _users[index.row()]->avatar(); + } else if (role == IsCurrentUserRole) { + return _users[index.row()]->isCurrentUser(); + } else if (role == IsConnectedRole) { + return _users[index.row()]->isConnected(); + } else if (role == IdRole) { + return index.row(); + } + return QVariant(); +} + +QHash UserModel::roleNames() const +{ + QHash roles; + roles[NameRole] = "name"; + roles[ServerRole] = "server"; + roles[AvatarRole] = "avatar"; + roles[IsCurrentUserRole] = "isCurrentUser"; + roles[IsConnectedRole] = "isConnected"; + roles[IdRole] = "id"; + return roles; +} + +ActivityListModel *UserModel::currentActivityModel() +{ + return _users[currentUserIndex()]->getActivityModel(); +} + +bool UserModel::currentUserHasActivities() +{ + return _users[currentUserIndex()]->hasActivities(); +} + +void UserModel::fetchCurrentActivityModel() +{ + _users[currentUserId()]->slotRefresh(); +} + +AccountAppList UserModel::appList() const +{ + if (_users.count() >= 1) { + return _users[_currentUserId]->appList(); + } else { + return AccountAppList(); + } +} + +/*-------------------------------------------------------------------------------------*/ + +ImageProvider::ImageProvider() + : QQuickImageProvider(QQuickImageProvider::Image) +{ +} + +QImage ImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) +{ + Q_UNUSED(size) + Q_UNUSED(requestedSize) + + if (id == "currentUser") { + return UserModel::instance()->currentUserAvatar(); + } else { + int uid = id.toInt(); + return UserModel::instance()->avatarById(uid); + } +} + +/*-------------------------------------------------------------------------------------*/ + +UserAppsModel *UserAppsModel::_instance = nullptr; + +UserAppsModel *UserAppsModel::instance() +{ + if (_instance == nullptr) { + _instance = new UserAppsModel(); + } + return _instance; +} + +UserAppsModel::UserAppsModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +void UserAppsModel::buildAppList() +{ + if (rowCount() > 0) { + beginRemoveRows(QModelIndex(), 0, rowCount() - 1); + _apps.clear(); + endRemoveRows(); + } + + if(UserModel::instance()->appList().count() > 0) { + foreach(AccountApp *app, UserModel::instance()->appList()) { + // Filter out Talk because we have a dedicated button for it + if(app->id() == QLatin1String("spreed")) + continue; + + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + _apps << app; + endInsertRows(); + } + } +} + +void UserAppsModel::openAppUrl(const QUrl &url) +{ + QDesktopServices::openUrl(url); +} + +int UserAppsModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return _apps.count(); +} + +QVariant UserAppsModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= _apps.count()) { + return QVariant(); + } + + if (role == NameRole) { + return _apps[index.row()]->name(); + } else if (role == UrlRole) { + return _apps[index.row()]->url(); + } else if (role == IconUrlRole) { + return _apps[index.row()]->iconUrl().toString(); + } + return QVariant(); +} + +QHash UserAppsModel::roleNames() const +{ + QHash roles; + roles[NameRole] = "appName"; + roles[UrlRole] = "appUrl"; + roles[IconUrlRole] = "appIconUrl"; + return roles; +} + +} diff --git a/src/gui/tray/UserModel.h b/src/gui/tray/UserModel.h new file mode 100644 index 000000000000..36f5af4b3942 --- /dev/null +++ b/src/gui/tray/UserModel.h @@ -0,0 +1,183 @@ +#ifndef USERMODEL_H +#define USERMODEL_H + +#include +#include +#include +#include +#include + +#include "ActivityListModel.h" +#include "accountmanager.h" +#include "folderman.h" +#include + +namespace OCC { + +class User : public QObject +{ + Q_OBJECT +public: + User(AccountStatePtr &account, const bool &isCurrent = false, QObject* parent = 0); + + AccountPtr account() const; + + bool isConnected() const; + bool isCurrentUser() const; + void setCurrentUser(const bool &isCurrent); + Folder *getFolder(); + ActivityListModel *getActivityModel(); + void openLocalFolder(); + QString name() const; + QString server(bool shortened = true) const; + bool serverHasTalk() const; + bool hasActivities() const; + AccountAppList appList() const; + QImage avatar(bool whiteBg = false) const; + QString id() const; + void login() const; + void logout() const; + void removeAccount() const; + +signals: + void guiLog(const QString &, const QString &); + +public slots: + void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item); + void slotProgressInfo(const QString &folder, const ProgressInfo &progress); + void slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category); + void slotNotificationRequestFinished(int statusCode); + void slotNotifyNetworkError(QNetworkReply *reply); + void slotEndNotificationRequest(int replyCode); + void slotNotifyServerFinished(const QString &reply, int replyCode); + void slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row); + void slotBuildNotificationDisplay(const ActivityList &list); + void slotRefreshNotifications(); + void slotRefreshActivities(); + void slotRefresh(); + void slotRefreshImmediately(); + void setNotificationRefreshInterval(std::chrono::milliseconds interval); + void slotRebuildNavigationAppList(); + +private: + AccountStatePtr _account; + bool _isCurrentUser; + ActivityListModel *_activityModel; + ActivityList _blacklistedNotifications; + + QTimer _notificationCheckTimer; + QHash _timeSinceLastCheck; + + QElapsedTimer _guiLogTimer; + QSet _guiLoggedNotifications; + + // number of currently running notification requests. If non zero, + // no query for notifications is started. + int _notificationRequestsRunning; +}; + +class UserModel : public QAbstractListModel +{ + Q_OBJECT +public: + static UserModel *instance(); + virtual ~UserModel() {}; + + void addUser(AccountStatePtr &user, const bool &isCurrent = false); + int currentUserIndex(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + QImage avatarById(const int &id); + + Q_INVOKABLE void fetchCurrentActivityModel(); + Q_INVOKABLE void openCurrentAccountLocalFolder(); + Q_INVOKABLE void openCurrentAccountTalk(); + Q_INVOKABLE void openCurrentAccountServer(); + Q_INVOKABLE QImage currentUserAvatar(); + Q_INVOKABLE int numUsers(); + Q_INVOKABLE QString currentUserName(); + Q_INVOKABLE QString currentUserServer(); + Q_INVOKABLE bool currentUserHasActivities(); + Q_INVOKABLE bool currentServerHasTalk(); + Q_INVOKABLE int currentUserId(); + Q_INVOKABLE bool isUserConnected(const int &id); + Q_INVOKABLE void switchCurrentUser(const int &id); + Q_INVOKABLE void login(const int &id); + Q_INVOKABLE void logout(const int &id); + Q_INVOKABLE void removeAccount(const int &id); + + ActivityListModel *currentActivityModel(); + + enum UserRoles { + NameRole = Qt::UserRole + 1, + ServerRole, + AvatarRole, + IsCurrentUserRole, + IsConnectedRole, + IdRole + }; + + AccountAppList appList() const; + +signals: + Q_INVOKABLE void addAccount(); + Q_INVOKABLE void refreshCurrentUserGui(); + Q_INVOKABLE void newUserSelected(); + +protected: + QHash roleNames() const override; + +private: + static UserModel *_instance; + UserModel(QObject *parent = 0); + QList _users; + int _currentUserId; + bool _init = true; + + void buildUserList(); +}; + +class ImageProvider : public QQuickImageProvider +{ +public: + ImageProvider(); + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; +}; + +class UserAppsModel : public QAbstractListModel +{ + Q_OBJECT +public: + static UserAppsModel *instance(); + virtual ~UserAppsModel() {}; + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + enum UserAppsRoles { + NameRole = Qt::UserRole + 1, + UrlRole, + IconUrlRole + }; + + void buildAppList(); + +public slots: + void openAppUrl(const QUrl &url); + +protected: + QHash roleNames() const override; + +private: + static UserAppsModel *_instance; + UserAppsModel(QObject *parent = 0); + + AccountAppList _apps; +}; + +} +#endif // USERMODEL_H diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml new file mode 100644 index 000000000000..5a040c970989 --- /dev/null +++ b/src/gui/tray/Window.qml @@ -0,0 +1,612 @@ +import QtQml 2.1 +import QtQml.Models 2.1 +import QtQuick 2.9 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 + +Window { + + id: trayWindow + visible: true + width: 400 + height: 510 + color: "transparent" + flags: Qt.FramelessWindowHint + + onActiveChanged: { + if(!active) { + trayWindow.hide(); + systrayBackend.setClosed(); + } + } + + onVisibleChanged: { + currentAccountAvatar.source = "" + currentAccountAvatar.source = "image://avatars/currentUser" + currentAccountUser.text = userModelBackend.currentUserName(); + currentAccountServer.text = userModelBackend.currentUserServer(); + trayWindowTalkButton.visible = userModelBackend.currentServerHasTalk() ? true : false; + currentAccountStateIndicator.source = "" + currentAccountStateIndicator.source = userModelBackend.isUserConnected(userModelBackend.currentUserId()) ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg" + + userLineInstantiator.active = false; + userLineInstantiator.active = true; + } + + Connections { + target: userModelBackend + onRefreshCurrentUserGui: { + currentAccountAvatar.source = "" + currentAccountAvatar.source = "image://avatars/currentUser" + currentAccountUser.text = userModelBackend.currentUserName(); + currentAccountServer.text = userModelBackend.currentUserServer(); + currentAccountStateIndicator.source = "" + currentAccountStateIndicator.source = userModelBackend.isUserConnected(userModelBackend.currentUserId()) ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg" + } + onNewUserSelected: { + accountMenu.close(); + trayWindowTalkButton.visible = userModelBackend.currentServerHasTalk() ? true : false; + } + } + + Connections { + target: systrayBackend + onShowWindow: { + accountMenu.close(); + trayWindow.show(); + trayWindow.raise(); + trayWindow.requestActivate(); + trayWindow.setX( systrayBackend.calcTrayWindowX()); + trayWindow.setY( systrayBackend.calcTrayWindowY()); + systrayBackend.setOpened(); + userModelBackend.fetchCurrentActivityModel(); + } + onHideWindow: { + trayWindow.hide(); + systrayBackend.setClosed(); + } + } + + Rectangle { + id: trayWindowBackground + anchors.fill: parent + radius: 10 + border.color: "#0082c9" + + Rectangle { + id: trayWindowHeaderBackground + anchors.left: trayWindowBackground.left + anchors.top: trayWindowBackground.top + height: 60 + width: parent.width + radius: 9 + color: "#0082c9" + + Rectangle { + anchors.left: trayWindowHeaderBackground.left + anchors.bottom: trayWindowHeaderBackground.bottom + height: 30 + width: parent.width + color: "#0082c9" + } + + RowLayout { + id: trayWindowHeaderLayout + spacing: 0 + anchors.fill: parent + + Button { + id: currentAccountButton + Layout.preferredWidth: 220 + Layout.preferredHeight: (trayWindowHeaderBackground.height) + display: AbstractButton.IconOnly + flat: true + + MouseArea { + id: accountBtnMouseArea + anchors.fill: parent + hoverEnabled: true + onContainsMouseChanged: { + currentAccountStateIndicatorBackground.color = (containsMouse ? "#009dd9" : "#0082c9") + } + onClicked: + { + syncPauseButton.text = systrayBackend.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all") + accountMenu.open() + } + + Menu { + id: accountMenu + x: (currentAccountButton.x + 2) + y: (currentAccountButton.y + currentAccountButton.height + 2) + width: (currentAccountButton.width - 2) + closePolicy: "CloseOnPressOutside" + + background: Rectangle { + border.color: "#0082c9" + radius: 2 + } + + onClosed: { + userLineInstantiator.active = false; + userLineInstantiator.active = true; + } + + Instantiator { + id: userLineInstantiator + model: userModelBackend + delegate: UserLine {} + onObjectAdded: accountMenu.insertItem(index, object) + onObjectRemoved: accountMenu.removeItem(object) + } + + MenuItem { + id: addAccountButton + height: 50 + + RowLayout { + width: addAccountButton.width + height: addAccountButton.height + spacing: 0 + + Image { + Layout.leftMargin: 14 + verticalAlignment: Qt.AlignCenter + source: "qrc:///client/theme/black/add.svg" + sourceSize.width: openLocalFolderButton.icon.width + sourceSize.height: openLocalFolderButton.icon.height + } + Label { + Layout.leftMargin: 14 + text: qsTr("Add account") + color: "black" + font.pixelSize: 12 + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + } + onClicked: userModelBackend.addAccount() + } + + MenuSeparator { id: accountMenuSeparator } + + MenuItem { + id: syncPauseButton + font.pixelSize: 12 + onClicked: systrayBackend.pauseResumeSync() + } + + MenuItem { + text: qsTr("Open settings") + font.pixelSize: 12 + onClicked: systrayBackend.openSettings() + } + + MenuItem { + text: qsTr("Help") + font.pixelSize: 12 + onClicked: systrayBackend.openHelp() + } + + MenuItem { + text: qsTr("Quit Nextcloud") + font.pixelSize: 12 + onClicked: systrayBackend.shutdown() + } + } + } + + background: + Item { + id: leftHoverContainer + height: currentAccountButton.height + width: currentAccountButton.width + Rectangle { + width: currentAccountButton.width / 2 + height: currentAccountButton.height / 2 + color: "transparent" + clip: true + Rectangle { + width: currentAccountButton.width + height: currentAccountButton.height + radius: 10 + color: "white" + opacity: 0.2 + visible: accountBtnMouseArea.containsMouse + } + } + Rectangle { + width: currentAccountButton.width / 2 + height: currentAccountButton.height / 2 + anchors.bottom: leftHoverContainer.bottom + color: "white" + opacity: 0.2 + visible: accountBtnMouseArea.containsMouse + } + Rectangle { + width: currentAccountButton.width / 2 + height: currentAccountButton.height / 2 + anchors.right: leftHoverContainer.right + color: "white" + opacity: 0.2 + visible: accountBtnMouseArea.containsMouse + } + Rectangle { + width: currentAccountButton.width / 2 + height: currentAccountButton.height / 2 + anchors.right: leftHoverContainer.right + anchors.bottom: leftHoverContainer.bottom + color: "white" + opacity: 0.2 + visible: accountBtnMouseArea.containsMouse + } + } + + RowLayout { + id: accountControlRowLayout + height: currentAccountButton.height + width: currentAccountButton.width + spacing: 0 + Image { + id: currentAccountAvatar + Layout.leftMargin: 8 + verticalAlignment: Qt.AlignCenter + cache: false + source: "image://avatars/currentUser" + Layout.preferredHeight: (trayWindowHeaderBackground.height -16) + Layout.preferredWidth: (trayWindowHeaderBackground.height -16) + Rectangle { + id: currentAccountStateIndicatorBackground + width: currentAccountStateIndicator.sourceSize.width + 2 + height: width + anchors.bottom: currentAccountAvatar.bottom + anchors.right: currentAccountAvatar.right + color: "#0082c9" + radius: width*0.5 + } + Image { + id: currentAccountStateIndicator + source: userModelBackend.isUserConnected(userModelBackend.currentUserId()) ? "qrc:///client/theme/colored/state-ok.svg" : "qrc:///client/theme/colored/state-offline.svg" + cache: false + x: currentAccountStateIndicatorBackground.x + 1 + y: currentAccountStateIndicatorBackground.y + 1 + sourceSize.width: 16 + sourceSize.height: 16 + } + } + + Column { + id: accountLabels + spacing: 4 + Layout.alignment: Qt.AlignLeft + Layout.leftMargin: 6 + Label { + id: currentAccountUser + width: 128 + text: userModelBackend.currentUserName() + elide: Text.ElideRight + color: "white" + font.pixelSize: 12 + font.bold: true + } + Label { + id: currentAccountServer + width: 128 + text: userModelBackend.currentUserServer() + elide: Text.ElideRight + color: "white" + font.pixelSize: 10 + } + } + + Image { + Layout.alignment: Qt.AlignRight + verticalAlignment: Qt.AlignCenter + Layout.margins: 8 + source: "qrc:///client/theme/white/caret-down.svg" + sourceSize.width: 20 + sourceSize.height: 20 + } + } + } + + Item { + id: trayWindowHeaderSpacer + Layout.fillWidth: true + } + + Button { + id: openLocalFolderButton + Layout.alignment: Qt.AlignRight + display: AbstractButton.IconOnly + Layout.preferredWidth: (trayWindowHeaderBackground.height) + Layout.preferredHeight: (trayWindowHeaderBackground.height) + flat: true + + icon.source: "qrc:///client/theme/white/folder.svg" + icon.color: "transparent" + + MouseArea { + id: folderBtnMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: + { + userModelBackend.openCurrentAccountLocalFolder(); + } + } + + background: + Rectangle { + color: folderBtnMouseArea.containsMouse ? "white" : "transparent" + opacity: 0.2 + } + } + + Button { + id: trayWindowTalkButton + Layout.alignment: Qt.AlignRight + display: AbstractButton.IconOnly + Layout.preferredWidth: (trayWindowHeaderBackground.height) + Layout.preferredHeight: (trayWindowHeaderBackground.height) + flat: true + visible: userModelBackend.currentServerHasTalk() ? true : false + + icon.source: "qrc:///client/theme/white/talk-app.svg" + icon.color: "transparent" + + MouseArea { + id: talkBtnMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: + { + userModelBackend.openCurrentAccountTalk(); + } + } + + background: + Rectangle { + color: talkBtnMouseArea.containsMouse ? "white" : "transparent" + opacity: 0.2 + } + } + + Button { + id: trayWindowAppsButton + Layout.alignment: Qt.AlignRight + display: AbstractButton.IconOnly + Layout.preferredWidth: (trayWindowHeaderBackground.height) + Layout.preferredHeight: (trayWindowHeaderBackground.height) + flat: true + + icon.source: "qrc:///client/theme/white/more-apps.svg" + icon.color: "transparent" + + MouseArea { + id: appsBtnMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: + { + /* + // The count() property was introduced in QtQuick.Controls 2.3 (Qt 5.10) + // so we handle this with userModelBackend.openCurrentAccountServer() + // + // See UserModel::openCurrentAccountServer() to disable this workaround + // in the future for Qt >= 5.10 + + if(appsMenu.count() > 0) { + appsMenu.popup(); + } else { + userModelBackend.openCurrentAccountServer(); + } + */ + + appsMenu.open(); + userModelBackend.openCurrentAccountServer(); + } + + Menu { + id: appsMenu + y: (trayWindowAppsButton.y + trayWindowAppsButton.height + 2) + width: (trayWindowAppsButton.width * 3) + closePolicy: "CloseOnPressOutside" + + background: Rectangle { + border.color: "#0082c9" + radius: 2 + } + + Instantiator { + id: appsMenuInstantiator + model: appsMenuModelBackend + onObjectAdded: appsMenu.insertItem(index, object) + onObjectRemoved: appsMenu.removeItem(object) + delegate: MenuItem { + text: appName + font.pixelSize: 12 + icon.source: appIconUrl + onTriggered: appsMenuModelBackend.openAppUrl(appUrl) + } + } + } + } + + background: + Item { + id: rightHoverContainer + height: trayWindowAppsButton.height + width: trayWindowAppsButton.width + Rectangle { + width: trayWindowAppsButton.width / 2 + height: trayWindowAppsButton.height / 2 + color: "white" + opacity: 0.2 + visible: appsBtnMouseArea.containsMouse + } + Rectangle { + width: trayWindowAppsButton.width / 2 + height: trayWindowAppsButton.height / 2 + anchors.bottom: rightHoverContainer.bottom + color: "white" + opacity: 0.2 + visible: appsBtnMouseArea.containsMouse + } + Rectangle { + width: trayWindowAppsButton.width / 2 + height: trayWindowAppsButton.height / 2 + anchors.bottom: rightHoverContainer.bottom + anchors.right: rightHoverContainer.right + color: "white" + opacity: 0.2 + visible: appsBtnMouseArea.containsMouse + } + Rectangle { + id: rightHoverContainerClipper + anchors.right: rightHoverContainer.right + width: trayWindowAppsButton.width / 2 + height: trayWindowAppsButton.height / 2 + color: "transparent" + clip: true + Rectangle { + width: trayWindowAppsButton.width + height: trayWindowAppsButton.height + anchors.right: rightHoverContainerClipper.right + radius: 10 + color: "white" + opacity: 0.2 + visible: appsBtnMouseArea.containsMouse + } + } + } + } + } + } // Rectangle trayWindowHeaderBackground + + ListView { + id: activityListView + anchors.top: trayWindowHeaderBackground.bottom + width: trayWindowBackground.width + height: trayWindowBackground.height - trayWindowHeaderBackground.height + clip: true + ScrollBar.vertical: ScrollBar { + id: listViewScrollbar + } + + model: activityModel + + delegate: RowLayout { + id: activityItem + width: activityListView.width + height: trayWindowHeaderLayout.height + spacing: 0 + + Image { + id: activityIcon + Layout.leftMargin: 8 + Layout.rightMargin: 8 + Layout.preferredWidth: activityButton1.icon.width + Layout.preferredHeight: activityButton1.icon.height + verticalAlignment: Qt.AlignCenter + cache: true + source: icon + sourceSize.height: 64 + sourceSize.width: 64 + } + Column { + id: activityTextColumn + spacing: 4 + Layout.alignment: Qt.AlignLeft + Text { + id: activityTextTitle + text: (type === "Activity" || type === "Notification") ? subject : message + width: 240 + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8 + elide: Text.ElideRight + font.pixelSize: 12 + color: activityTextTitleColor + } + + Text { + id: activityTextInfo + text: (type === "Activity" || type === "File" || type === "Sync") ? displaypath : message + height: (text === "") ? 0 : activityTextTitle.height + width: 240 + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8 + elide: Text.ElideRight + font.pixelSize: 10 + } + + Text { + id: activityTextDateTime + text: dateTime + height: (text === "") ? 0 : activityTextTitle.height + width: 240 + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8 + elide: Text.ElideRight + font.pixelSize: 10 + color: "#808080" + } + } + Item { + id: activityItemFiller + Layout.fillWidth: true + } + Button { + id: activityButton1 + Layout.preferredWidth: (path === "") ? 0 : activityItem.height + Layout.preferredHeight: activityItem.height + Layout.alignment: Qt.AlignRight + flat: true + hoverEnabled: false + visible: (path === "") ? false : true + display: AbstractButton.IconOnly + icon.source: "qrc:///client/resources/files.svg" + icon.color: "transparent" + + onClicked: { + Qt.openUrlExternally(path) + } + } + Button { + id: activityButton2 + Layout.preferredWidth: (link === "") ? 0 : activityItem.height + Layout.preferredHeight: activityItem.height + Layout.alignment: Qt.AlignRight + flat: true + hoverEnabled: false + visible: (link === "") ? false : true + display: AbstractButton.IconOnly + icon.source: "qrc:///client/resources/public.svg" + icon.color: "transparent" + + onClicked: { + Qt.openUrlExternally(link) + } + } + } + + /*add: Transition { + NumberAnimation { properties: "y"; from: -60; duration: 100; easing.type: Easing.Linear } + } + + remove: Transition { + NumberAnimation { property: "opacity"; from: 1.0; to: 0; duration: 100 } + } + + removeDisplaced: Transition { + SequentialAnimation { + PauseAnimation { duration: 100} + NumberAnimation { properties: "y"; duration: 100; easing.type: Easing.Linear } + } + } + + displaced: Transition { + NumberAnimation { properties: "y"; duration: 100; easing.type: Easing.Linear } + }*/ + } + + } // Rectangle trayWindowBackground +} diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 39df7d1677f8..d0c5f727f7db 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -103,7 +103,8 @@ bool Capabilities::isValid() const return !_capabilities.isEmpty(); } -bool Capabilities::hasActivities() const { +bool Capabilities::hasActivities() const +{ return _capabilities.contains("activity"); } diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index c728172e6a8a..03d1dbf7244a 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -376,14 +376,14 @@ void OwncloudPropagator::start(const SyncFileItemVector &items, Q_ASSERT(std::is_sorted(items.begin(), items.end())); } else if (hasChange) { Q_ASSERT(std::is_sorted(items.begin(), items.end(), - [](const SyncFileItemVector::const_reference &a, const SyncFileItemVector::const_reference &b) -> bool { + [](SyncFileItemVector::const_reference &a, SyncFileItemVector::const_reference &b) -> bool { return ((a->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) && (b->_instruction != CSYNC_INSTRUCTION_TYPE_CHANGE)); })); Q_ASSERT(std::is_sorted(items.begin(), items.begin() + lastChangeInstruction)); if (hasDelete) { Q_ASSERT(std::is_sorted(items.begin() + (lastChangeInstruction + 1), items.end(), - [](const SyncFileItemVector::const_reference &a, const SyncFileItemVector::const_reference &b) -> bool { + [](SyncFileItemVector::const_reference &a, SyncFileItemVector::const_reference &b) -> bool { return ((a->_instruction == CSYNC_INSTRUCTION_REMOVE) && (b->_instruction != CSYNC_INSTRUCTION_REMOVE)); })); Q_ASSERT(std::is_sorted(items.begin() + (lastChangeInstruction + 1), items.begin() + lastDeleteInstruction)); diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 24732d9753b0..42e45b5c8596 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -1078,7 +1078,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult) // Get CHANGE instructions to the top first if (syncItems.count() > 0) { std::sort(syncItems.begin(), syncItems.end(), - [](const SyncFileItemVector::const_reference &a, const SyncFileItemVector::const_reference &b) -> bool { + [](SyncFileItemVector::const_reference &a, SyncFileItemVector::const_reference &b) -> bool { return ((a->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) && (b->_instruction != CSYNC_INSTRUCTION_TYPE_CHANGE)); }); if (syncItems.at(0)->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) { @@ -1089,7 +1089,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult) std::sort(syncItems.begin(), syncItems.begin() + lastChangeInstruction); if (syncItems.count() > lastChangeInstruction) { std::sort(syncItems.begin() + (lastChangeInstruction + 1), syncItems.end(), - [](const SyncFileItemVector::const_reference &a, const SyncFileItemVector::const_reference &b) -> bool { + [](SyncFileItemVector::const_reference &a, SyncFileItemVector::const_reference &b) -> bool { return ((a->_instruction == CSYNC_INSTRUCTION_REMOVE) && (b->_instruction != CSYNC_INSTRUCTION_REMOVE)); }); if (syncItems.at(lastChangeInstruction + 1)->_instruction == CSYNC_INSTRUCTION_REMOVE) { diff --git a/src/libsync/theme.cpp b/src/libsync/theme.cpp index 8b2b15e5c061..08bf1152180f 100644 --- a/src/libsync/theme.cpp +++ b/src/libsync/theme.cpp @@ -123,11 +123,11 @@ QIcon Theme::applicationIcon() const * helper to load a icon from either the icon theme the desktop provides or from * the apps Qt resources. */ -QIcon Theme::themeIcon(const QString &name, bool sysTray, bool sysTrayMenuVisible) const +QIcon Theme::themeIcon(const QString &name, bool sysTray) const { QString flavor; if (sysTray) { - flavor = systrayIconFlavor(_mono, sysTrayMenuVisible); + flavor = systrayIconFlavor(_mono); } else { flavor = QLatin1String("colored"); } @@ -170,7 +170,7 @@ QIcon Theme::themeIcon(const QString &name, bool sysTray, bool sysTrayMenuVisibl #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) // This defines the icon as a template and enables automatic macOS color handling // See https://bugreports.qt.io/browse/QTBUG-42109 - cached.setIsMask(_mono && sysTray && !sysTrayMenuVisible); + cached.setIsMask(_mono && sysTray); #endif #endif @@ -270,18 +270,11 @@ QString Theme::defaultClientFolder() const return appName(); } -QString Theme::systrayIconFlavor(bool mono, bool sysTrayMenuVisible) const +QString Theme::systrayIconFlavor(bool mono) const { - Q_UNUSED(sysTrayMenuVisible) QString flavor; if (mono) { flavor = Utility::hasDarkSystray() ? QLatin1String("white") : QLatin1String("black"); - -#ifdef Q_OS_MAC - if (sysTrayMenuVisible) { - flavor = QLatin1String("white"); - } -#endif } else { flavor = QLatin1String("colored"); } @@ -395,7 +388,7 @@ QVariant Theme::customMedia(CustomMediaType type) return re; } -QIcon Theme::syncStateIcon(SyncResult::Status status, bool sysTray, bool sysTrayMenuVisible) const +QIcon Theme::syncStateIcon(SyncResult::Status status, bool sysTray) const { // FIXME: Mind the size! QString statusIcon; @@ -427,7 +420,7 @@ QIcon Theme::syncStateIcon(SyncResult::Status status, bool sysTray, bool sysTray statusIcon = QLatin1String("state-error"); } - return themeIcon(statusIcon, sysTray, sysTrayMenuVisible); + return themeIcon(statusIcon, sysTray); } QIcon Theme::folderDisabledIcon() const @@ -435,9 +428,9 @@ QIcon Theme::folderDisabledIcon() const return themeIcon(QLatin1String("state-pause")); } -QIcon Theme::folderOfflineIcon(bool sysTray, bool sysTrayMenuVisible) const +QIcon Theme::folderOfflineIcon(bool sysTray) const { - return themeIcon(QLatin1String("state-offline"), sysTray, sysTrayMenuVisible); + return themeIcon(QLatin1String("state-offline"), sysTray); } QColor Theme::wizardHeaderTitleColor() const diff --git a/src/libsync/theme.h b/src/libsync/theme.h index 966c9b5c9e8f..12a643d0099d 100644 --- a/src/libsync/theme.h +++ b/src/libsync/theme.h @@ -93,10 +93,10 @@ class OWNCLOUDSYNC_EXPORT Theme : public QObject /** * get an sync state icon */ - virtual QIcon syncStateIcon(SyncResult::Status, bool sysTray = false, bool sysTrayMenuVisible = false) const; + virtual QIcon syncStateIcon(SyncResult::Status, bool sysTray = false) const; virtual QIcon folderDisabledIcon() const; - virtual QIcon folderOfflineIcon(bool sysTray = false, bool sysTrayMenuVisible = false) const; + virtual QIcon folderOfflineIcon(bool sysTray = false) const; virtual QIcon applicationIcon() const; #endif @@ -166,7 +166,7 @@ class OWNCLOUDSYNC_EXPORT Theme : public QObject virtual QString enforcedLocale() const { return QString(); } /** colored, white or black */ - QString systrayIconFlavor(bool mono, bool sysTrayMenuVisible = false) const; + QString systrayIconFlavor(bool mono) const; #ifndef TOKEN_AUTH_ONLY /** @@ -449,7 +449,7 @@ class OWNCLOUDSYNC_EXPORT Theme : public QObject protected: #ifndef TOKEN_AUTH_ONLY - QIcon themeIcon(const QString &name, bool sysTray = false, bool sysTrayMenuVisible = false) const; + QIcon themeIcon(const QString &name, bool sysTray = false) const; #endif Theme(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 55e5327e6887..501ec9039af5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -66,6 +66,8 @@ list(APPEND FolderMan_SRC ../src/gui/guiutility.cpp ) list(APPEND FolderMan_SRC ../src/gui/navigationpanehelper.cpp ) list(APPEND FolderMan_SRC ../src/gui/connectionvalidator.cpp ) list(APPEND FolderMan_SRC ../src/gui/clientproxy.cpp ) +list(APPEND FolderMan_SRC ../src/gui/ocsjob.cpp ) +list(APPEND FolderMan_SRC ../src/gui/ocsnavigationappsjob.cpp ) list(APPEND FolderMan_SRC ../src/gui/accountstate.cpp ) list(APPEND FolderMan_SRC ../src/gui/remotewipe.cpp ) list(APPEND FolderMan_SRC ${FolderWatcher_SRC}) @@ -76,11 +78,13 @@ SET(RemoteWipe_SRC ../src/gui/remotewipe.cpp) list(APPEND RemoteWipe_SRC ../src/gui/clientproxy.cpp ) list(APPEND RemoteWipe_SRC ../src/gui/guiutility.cpp ) list(APPEND RemoteWipe_SRC ../src/gui/connectionvalidator.cpp ) +list(APPEND RemoteWipe_SRC ../src/gui/ocsjob.cpp ) +list(APPEND RemoteWipe_SRC ../src/gui/ocsnavigationappsjob.cpp ) list(APPEND RemoteWipe_SRC ../src/gui/accountstate.cpp ) list(APPEND RemoteWipe_SRC ../src/gui/socketapi.cpp ) list(APPEND RemoteWipe_SRC ../src/gui/folder.cpp ) list(APPEND RemoteWipe_SRC ../src/gui/syncrunfilelog.cpp ) -list(APPEND RemoteWipe_SRC ../src/gui/folderwatcher_linux.cpp ) +list(APPEND RemoteWipe_SRC ${FolderWatcher_SRC} ) list(APPEND RemoteWipe_SRC ../src/gui/folderwatcher.cpp ) list(APPEND RemoteWipe_SRC ${RemoteWipe_SRC}) list(APPEND RemoteWipe_SRC stubremotewipe.cpp ) diff --git a/theme.qrc b/theme.qrc index 006056b87bee..2a2881e96302 100644 --- a/theme.qrc +++ b/theme.qrc @@ -43,7 +43,7 @@ theme/white/state-sync-64.png theme/white/state-sync-128.png theme/white/state-sync-256.png - theme/black/state-error-32.png + theme/black/state-error-32.png theme/black/state-error-64.png theme/black/state-error-128.png theme/black/state-error-256.png @@ -80,7 +80,7 @@ theme/colored/state-warning-128.png theme/colored/state-warning-256.png theme/black/control-next.svg - theme/black/control-prev.svg + theme/black/control-prev.svg theme/black/state-error.svg theme/black/state-error-16.png theme/black/state-offline.svg @@ -101,8 +101,8 @@ theme/black/state-warning-64.png theme/black/state-warning-128.png theme/black/state-warning-256.png - theme/white/control-next.svg - theme/white/control-prev.svg + theme/white/control-next.svg + theme/white/control-prev.svg theme/white/state-error.svg theme/white/state-error-16.png theme/white/state-offline.svg @@ -131,5 +131,17 @@ theme/colored/wizard-nextcloud@2x.png theme/colored/wizard-talk.png theme/colored/wizard-talk@2x.png + theme/white/folder.svg + theme/white/more-apps.svg + theme/white/talk-app.svg + theme/white/caret-down.svg + theme/black/caret-down.svg + theme/white/user.svg + theme/black/user.svg + theme/white/add.svg + theme/black/add.svg + theme/black/activity.svg + theme/black/bell.svg + theme/black/state-info.svg
diff --git a/theme/black/activity.svg b/theme/black/activity.svg new file mode 100644 index 000000000000..b6282ba77d28 --- /dev/null +++ b/theme/black/activity.svg @@ -0,0 +1,2 @@ + + diff --git a/theme/black/add.svg b/theme/black/add.svg new file mode 100644 index 000000000000..4b2d2133078e --- /dev/null +++ b/theme/black/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/black/bell.svg b/theme/black/bell.svg new file mode 100644 index 000000000000..9de878f8d313 --- /dev/null +++ b/theme/black/bell.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/theme/black/caret-down.svg b/theme/black/caret-down.svg new file mode 100644 index 000000000000..bc3a4e0c5063 --- /dev/null +++ b/theme/black/caret-down.svg @@ -0,0 +1 @@ + diff --git a/theme/black/state-info.svg b/theme/black/state-info.svg new file mode 100644 index 000000000000..762de0370ff5 --- /dev/null +++ b/theme/black/state-info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/black/user.svg b/theme/black/user.svg new file mode 100644 index 000000000000..14aef2cd9803 --- /dev/null +++ b/theme/black/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/white/add.svg b/theme/white/add.svg new file mode 100644 index 000000000000..0604ed0994b2 --- /dev/null +++ b/theme/white/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/white/caret-down.svg b/theme/white/caret-down.svg new file mode 100644 index 000000000000..c97d6e264074 --- /dev/null +++ b/theme/white/caret-down.svg @@ -0,0 +1 @@ + diff --git a/theme/white/folder.svg b/theme/white/folder.svg new file mode 100644 index 000000000000..003e8b3fb80d --- /dev/null +++ b/theme/white/folder.svg @@ -0,0 +1 @@ + diff --git a/theme/white/more-apps.svg b/theme/white/more-apps.svg new file mode 100644 index 000000000000..ee5b522769f8 --- /dev/null +++ b/theme/white/more-apps.svg @@ -0,0 +1 @@ + diff --git a/theme/white/talk-app.svg b/theme/white/talk-app.svg new file mode 100644 index 000000000000..867f2ec449b5 --- /dev/null +++ b/theme/white/talk-app.svg @@ -0,0 +1 @@ + diff --git a/theme/white/user.svg b/theme/white/user.svg new file mode 100644 index 000000000000..991242ce44ba --- /dev/null +++ b/theme/white/user.svg @@ -0,0 +1 @@ + \ No newline at end of file