Merge pull request #1565 from nextcloud/qml-tray-menu

New tray window
This commit is contained in:
Michael Schuster 2020-01-18 13:18:45 +01:00 committed by GitHub
commit 2039872ee5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 2632 additions and 2444 deletions

View file

@ -2,6 +2,7 @@
<qresource prefix="/client">
<file>resources/settings.png</file>
<file>resources/settings@2x.png</file>
<file>resources/activity.svg</file>
<file>resources/activity.png</file>
<file>resources/activity@2x.png</file>
<file>resources/network.png</file>
@ -11,6 +12,7 @@
<file>resources/lock-https.png</file>
<file>resources/lock-https@2x.png</file>
<file>resources/account.png</file>
<file>resources/account.svg</file>
<file>resources/more.svg</file>
<file>resources/delete.png</file>
<file>resources/close.svg</file>
@ -28,7 +30,14 @@
<file>resources/copy.svg</file>
<file>resources/state-sync.svg</file>
<file>resources/add.png</file>
<file>resources/add-color.svg</file>
<file>resources/state-info.svg</file>
<file>resources/change.svg</file>
<file>resources/delete-color.svg</file>
</qresource>
<qresource prefix="/"/>
<qresource prefix="/qml">
<file>src/gui/tray/Window.qml</file>
<file>src/gui/tray/UserLine.qml</file>
</qresource>
</RCC>

1
resources/add-color.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path fill="#00d400" d="M9.02 13.98h-2v-5h-5v-2h5v-5h2v5l5-.028V8.98h-5z"/></svg>

After

Width:  |  Height:  |  Size: 179 B

1
resources/change.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" version="1.1" height="16"><path d="m8 2c-2.142 0-4.125 1.145-5.196 3l1.948 1.125c0.671-1.162 1.906-1.875 3.2476-1.875 1.1906 0 2.297 0.56157 3 1.5l-1.5 1.5h4.5v-4.5l-1.406 1.406c-1.129-1.348-2.802-2.1563-4.594-2.1563z"/><path d="m2 8.75v4.5l1.408-1.41c1.116 1.334 2.817 2.145 4.592 2.16 2.16 0.01827 4.116-1.132 5.196-3.002l-1.948-1.125c-0.677 1.171-1.9005 1.886-3.248 1.875-1.18-0.01-2.3047-0.572-3-1.5l1.5-1.5z"/></svg>

After

Width:  |  Height:  |  Size: 493 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16"><path d="m3.0503 4.4645 3.5355 3.5355-3.5355 3.536 1.4142 1.414 3.5355-3.5358 3.536 3.5358 1.414-1.414-3.5358-3.536 3.5358-3.5355-1.414-1.4142-3.536 3.5355-3.5355-3.5355-1.4142 1.4142z" fill="#d40000"/></svg>

After

Width:  |  Height:  |  Size: 306 B

View file

@ -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));

View file

@ -20,6 +20,7 @@
#ifndef UTILITY_H
#define UTILITY_H
#include "ocsynclib.h"
#include <QString>
#include <QByteArray>
@ -29,6 +30,7 @@
#include <QMap>
#include <QUrl>
#include <QUrlQuery>
#include <QtQuick/QQuickImageProvider>
#include <functional>
#include <memory>

View file

@ -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} )

View file

@ -89,6 +89,9 @@ private:
// Adds an account to the tracked list, emitting accountAdded()
void addAccountState(AccountState *accountState);
AccountManager() {}
QList<AccountStatePtr> _accounts;
public slots:
/// Saves account data, not including the credentials
void saveAccount(Account *a);
@ -104,9 +107,5 @@ Q_SIGNALS:
void accountAdded(AccountState *account);
void accountRemoved(AccountState *account);
void removeAccountFolders(AccountState *account);
private:
AccountManager() {}
QList<AccountStatePtr> _accounts;
};
}

View file

@ -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,36 +204,13 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
_ui->encryptionMessage->hide();
}
connect(UserModel::instance(), &UserModel::addAccount,
this, &AccountSettings::slotOpenAccountWizard);
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);
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);
}
void AccountSettings::slotNewMnemonicGenerated()
{
_ui->encryptionMessage->setText(tr("This account supports end-to-end encryption"));
@ -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

View file

@ -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();

View file

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>582</width>
<width>581</width>
<height>557</height>
</rect>
</property>
@ -184,13 +184,6 @@
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="_accountToolbox">
<property name="text">
<string notr="true">...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View file

@ -20,6 +20,7 @@
#include "creds/httpcredentials.h"
#include "logger.h"
#include "configfile.h"
#include "ocsnavigationappsjob.h"
#include <QSettings>
#include <QTimer>
@ -27,6 +28,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QNetworkRequest>
#include <QBuffer>
@ -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 *>("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<QSettings> 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

View file

@ -29,9 +29,11 @@ namespace OCC {
class AccountState;
class Account;
class AccountApp;
class RemoteWipe;
typedef QExplicitlySharedDataPointer<AccountState> AccountStatePtr;
typedef QList<AccountApp*> AccountAppList;
/**
* @brief Extra info about an ownCloud server account.
@ -101,6 +103,11 @@ public:
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> _connectionValidator;
QByteArray _notificationsEtagResponseHeader;
@ -205,7 +219,34 @@ private:
*/
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 *)

View file

@ -1,407 +0,0 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "activityitemdelegate.h"
#include "activitylistmodel.h"
#include "folderstatusmodel.h"
#include "folderman.h"
#include "accountstate.h"
#include "activitydata.h"
#include <theme.h>
#include <account.h>
#include <QFileIconProvider>
#include <QPainter>
#include <QApplication>
#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 <QDesktopWidget>
#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<Activity::Type>(index.data(ActionRole));
QIcon actionIcon;
const ActivityListModel::ActionIcon icn = qvariant_cast<ActivityListModel::ActionIcon>(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<QString>(index.data(ObjectTypeRole));
QString actionText = qvariant_cast<QString>(index.data(ActionTextRole));
QString messageText = qvariant_cast<QString>(index.data(MessageRole));
QList<QVariant> customList = index.data(ActionsLinksRole).toList();
QString timeText = qvariant_cast<QString>(index.data(PointInTimeRole));
bool accountOnline = qvariant_cast<bool>(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<Activity::Type>(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

View file

@ -1,94 +0,0 @@
/*
* Copyright (C) by Klaas Freitag <freitag@kde.org>
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include <QStyledItemDelegate>
#include <QMouseEvent>
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

View file

@ -1,653 +0,0 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <QtGui>
#include <QtWidgets>
#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 <climits>
// 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<QString>(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<QString>(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<ActivityLink>(customItem);
if(actionLink._label == _accept){
qCWarning(lcActivity) << objectType << "action" << actionLink._label << "for" << qvariant_cast<QString>(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<QString>(index.data(ActivityItemDelegate::ActionTextRole));
}
}
}
void ActivityWidget::slotSecondaryButtonClickedOnListView(const QModelIndex &index){
QList<QVariant> customList = index.data(ActivityItemDelegate::ActionsLinksRole).toList();
QString objectType = index.data(ActivityItemDelegate::ObjectTypeRole).toString();
QList<ActivityLink> actionLinks;
foreach(QVariant customItem, customList){
actionLinks << qvariant_cast<ActivityLink>(customItem);
}
if(objectType == _remote_share && actionLinks.first()._label == _accept)
actionLinks.removeFirst();
if(qvariant_cast<Activity::Type>(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<QString>(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<QString>(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<Activity::Type>(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<QString> i(_accountsWithoutActivities);
while (i.hasNext()) {
t.append(tr("<br/>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<NotificationConfirmJob *>(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<NotificationConfirmJob *>(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();
}
}

View file

@ -1,165 +0,0 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#ifndef ACTIVITYWIDGET_H
#define ACTIVITYWIDGET_H
#include <QDialog>
#include <QDateTime>
#include <QLocale>
#include <QAbstractListModel>
#include <chrono>
#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<QString> _accountsWithoutActivities;
QElapsedTimer _guiLogTimer;
QSet<int> _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<AccountState *, QElapsedTimer> _timeSinceLastCheck;
AccountState *_accountState;
};
}
#endif // ActivityWIDGET_H

View file

@ -1,98 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::ActivityWidget</class>
<widget class="QWidget" name="OCC::ActivityWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>652</width>
<height>556</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListView" name="_activityList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="defaultDropAction">
<enum>Qt::IgnoreAction</enum>
</property>
<property name="resizeMode">
<enum>QListView::Adjust</enum>
</property>
<property name="viewMode">
<enum>QListView::ListMode</enum>
</property>
<property name="modelColumn">
<number>0</number>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="_bottomLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>_activityList</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View file

@ -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();
}
}

View file

@ -914,6 +914,8 @@ void Folder::slotItemCompleted(const SyncFileItemPtr &item)
_folderWatcher->removePath(path() + item->_file);
_folderWatcher->addPath(path() + item->destination());
break;
default:
break;
}
}

View file

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>785</width>
<width>516</width>
<height>523</height>
</rect>
</property>

View file

@ -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);
}

View file

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>563</width>
<width>516</width>
<height>444</height>
</rect>
</property>

View file

@ -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 <QX11Info>
#endif
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QQmlApplicationEngine>
#include <QQuickItem>
#include <QQmlContext>
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<QObject *>(_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<AccountStatePtr>(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<AccountStatePtr>(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<AccountStatePtr>(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()

View file

@ -63,9 +63,7 @@ public:
void setupCloudProviders();
bool cloudProviderApiAvailable();
#endif
/// Whether the tray menu is visible
bool contextMenuVisible() const;
void createTray();
signals:
void setupProxy();
@ -73,16 +71,10 @@ signals:
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<Systray> _tray;
QPointer<SettingsDialog> _settingsDialog;
QPointer<LogBrowser> _logBrowser;
// tray's menu
QScopedPointer<QMenu> _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<QMenu *> _accountMenus;
bool _workaroundShowAndHideTray = false;
bool _workaroundNoAboutToShowUpdate = false;
bool _workaroundFakeDoubleClick = false;
bool _workaroundManualVisibility = false;
QTimer _delayedTrayUpdateTimer;
QMap<QString, QPointer<ShareDialog>> _shareDialogs;
QAction *_actionNewAccountWizard;
QAction *_actionSettings;
QAction *_actionStatus;
QAction *_actionEstimate;
QAction *_actionRecent;
QAction *_actionHelp;
QAction *_actionQuit;
QAction *_actionCrash;
QMenu *_navLinksMenu;
QMap<AccountStatePtr, QJsonArray> _navApps;
QList<QAction *> _recentItemsActions;
Application *_app;

View file

@ -1,50 +0,0 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#ifndef SERVERNOTIFICATIONHANDLER_H
#define SERVERNOTIFICATIONHANDLER_H
#include <QtCore>
#include "activitywidget.h"
class QJsonDocument;
namespace OCC {
class ServerNotificationHandler : public QObject
{
Q_OBJECT
public:
explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr);
static QMap<int, QIcon> 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<JsonApiJob> _notificationJob;
AccountState *_accountState;
};
}
#endif // SERVERNOTIFICATIONHANDLER_H

View file

@ -23,7 +23,6 @@
#include "configfile.h"
#include "progressdispatcher.h"
#include "owncloudgui.h"
#include "activitywidget.h"
#include "accountmanager.h"
#include <QLabel>
@ -189,39 +188,13 @@ void SettingsDialog::showFirstPage()
}
}
void SettingsDialog::showActivityPage()
{
if (auto account = qvariant_cast<AccountState*>(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<AccountState*>(sender()));
}
void SettingsDialog::slotRefreshActivity(AccountState *accountState)
{
if (accountState->isConnected())
_activitySettings[accountState]->slotRefresh();
}
} // namespace OCC

View file

@ -37,7 +37,6 @@ class AccountSettings;
class Application;
class FolderMan;
class ownCloudGui;
class ActivitySettings;
/**
* @brief The SettingsDialog class
@ -55,11 +54,8 @@ public:
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:
QHash<Account *, QAction *> _actionForAccount;
QToolBar *_toolBar;
QMap<AccountState *, ActivitySettings *> _activitySettings;
ownCloudGui *_gui;
};

View file

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>693</width>
<width>516</width>
<height>457</height>
</rect>
</property>

View file

@ -12,9 +12,17 @@
* for more details.
*/
#include "accountmanager.h"
#include "systray.h"
#include "theme.h"
#include "config.h"
#include "tray/UserModel.h"
#include <QDesktopServices>
#include <QGuiApplication>
#include <QQmlComponent>
#include <QQmlEngine>
#include <QScreen>
#ifdef USE_FDO_NOTIFICATIONS
#include <QDBusConnection>
@ -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<QVariant> args = QList<QVariant>() << 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

View file

@ -16,6 +16,10 @@
#define SYSTRAY_H
#include <QSystemTrayIcon>
#include <QQmlContext>
#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

View file

@ -14,7 +14,7 @@
#include <QtCore>
#include "activitydata.h"
#include "ActivityData.h"
namespace OCC {

View file

@ -56,6 +56,7 @@ public:
Type _type;
qlonglong _id;
QString _fileAction;
QString _objectType;
QString _subject;
QString _message;
@ -64,6 +65,8 @@ public:
QUrl _link;
QDateTime _dateTime;
QString _accName;
QString _icon;
QString _iconData;
// Stores information about the error
int _status;

View file

@ -15,7 +15,6 @@
#include <QtCore>
#include <QAbstractListModel>
#include <QWidget>
#include <QIcon>
#include <QJsonObject>
#include <QJsonDocument>
@ -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<int, QByteArray> ActivityListModel::roleNames() const
{
QHash<int, QByteArray> 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());
if (folder) {
relPath.prepend(folder->remotePath());
}
list = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
if (list.count() > 0) {
return QVariant(list.at(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());
list = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
if (list.count() > 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<QVariant> 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;
}
}

View file

@ -17,7 +17,7 @@
#include <QtCore>
#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;
};
enum DataRole {
ActionIconRole = Qt::UserRole + 1,
UserIconRole,
AccountRole,
ObjectTypeRole,
ActionsLinksRole,
ActionTextRole,
ActionTextColorRole,
ActionRole,
MessageRole,
DisplayPathRole,
PathRole,
LinkRole,
PointInTimeRole,
AccountConnectedRole,
SyncFileStatusRole};
explicit ActivityListModel(AccountState *accountState, QWidget *parent = nullptr);
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<int, QByteArray> roleNames() const override;
private:
void startFetchJob();
void combineActivityLists();
@ -96,9 +103,12 @@ private:
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

View file

@ -1,18 +1,5 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "NotificationHandler.h"
#include "servernotificationhandler.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<int, QIcon> ServerNotificationHandler::iconCache;
QMap<int, QByteArray> 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<AccountState *>(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();
}
}
}

View file

@ -0,0 +1,36 @@
#ifndef NOTIFICATIONHANDLER_H
#define NOTIFICATIONHANDLER_H
#include <QtCore>
#include "UserModel.h"
class QJsonDocument;
namespace OCC {
class ServerNotificationHandler : public QObject
{
Q_OBJECT
public:
explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr);
static QMap<int, QByteArray> 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<JsonApiJob> _notificationJob;
AccountState *_accountState;
};
}
#endif // NOTIFICATIONHANDLER_H

156
src/gui/tray/UserLine.qml Normal file
View file

@ -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

868
src/gui/tray/UserModel.cpp Normal file
View file

@ -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 <QDesktopServices>
#include <QIcon>
#include <QMessageBox>
#include <QSvgRenderer>
#include <QPainter>
#include <QPushButton>
// 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<NotificationConfirmJob *>(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<NotificationConfirmJob *>(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("<p>Do you really want to remove the connection to the account <i>%1</i>?</p>"
"<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
.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<int, QByteArray> UserModel::roleNames() const
{
QHash<int, QByteArray> 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<int, QByteArray> UserAppsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "appName";
roles[UrlRole] = "appUrl";
roles[IconUrlRole] = "appIconUrl";
return roles;
}
}

183
src/gui/tray/UserModel.h Normal file
View file

@ -0,0 +1,183 @@
#ifndef USERMODEL_H
#define USERMODEL_H
#include <QAbstractListModel>
#include <QImage>
#include <QDateTime>
#include <QStringList>
#include <QQuickImageProvider>
#include "ActivityListModel.h"
#include "accountmanager.h"
#include "folderman.h"
#include <chrono>
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<AccountState *, QElapsedTimer> _timeSinceLastCheck;
QElapsedTimer _guiLogTimer;
QSet<int> _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<int, QByteArray> roleNames() const override;
private:
static UserModel *_instance;
UserModel(QObject *parent = 0);
QList<User*> _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<int, QByteArray> roleNames() const override;
private:
static UserAppsModel *_instance;
UserAppsModel(QObject *parent = 0);
AccountAppList _apps;
};
}
#endif // USERMODEL_H

612
src/gui/tray/Window.qml Normal file
View file

@ -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
}

View file

@ -103,7 +103,8 @@ bool Capabilities::isValid() const
return !_capabilities.isEmpty();
}
bool Capabilities::hasActivities() const {
bool Capabilities::hasActivities() const
{
return _capabilities.contains("activity");
}

View file

@ -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));

View file

@ -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) {

View file

@ -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

View file

@ -93,10 +93,10 @@ public:
/**
* 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 @@ public:
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 @@ public:
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();

View file

@ -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 )

View file

@ -43,7 +43,7 @@
<file>theme/white/state-sync-64.png</file>
<file>theme/white/state-sync-128.png</file>
<file>theme/white/state-sync-256.png</file>
<file>theme/black/state-error-32.png</file>
<file>theme/black/state-error-32.png</file>
<file>theme/black/state-error-64.png</file>
<file>theme/black/state-error-128.png</file>
<file>theme/black/state-error-256.png</file>
@ -80,7 +80,7 @@
<file>theme/colored/state-warning-128.png</file>
<file>theme/colored/state-warning-256.png</file>
<file>theme/black/control-next.svg</file>
<file>theme/black/control-prev.svg</file>
<file>theme/black/control-prev.svg</file>
<file>theme/black/state-error.svg</file>
<file>theme/black/state-error-16.png</file>
<file>theme/black/state-offline.svg</file>
@ -101,8 +101,8 @@
<file>theme/black/state-warning-64.png</file>
<file>theme/black/state-warning-128.png</file>
<file>theme/black/state-warning-256.png</file>
<file>theme/white/control-next.svg</file>
<file>theme/white/control-prev.svg</file>
<file>theme/white/control-next.svg</file>
<file>theme/white/control-prev.svg</file>
<file>theme/white/state-error.svg</file>
<file>theme/white/state-error-16.png</file>
<file>theme/white/state-offline.svg</file>
@ -131,5 +131,17 @@
<file>theme/colored/wizard-nextcloud@2x.png</file>
<file>theme/colored/wizard-talk.png</file>
<file>theme/colored/wizard-talk@2x.png</file>
<file>theme/white/folder.svg</file>
<file>theme/white/more-apps.svg</file>
<file>theme/white/talk-app.svg</file>
<file>theme/white/caret-down.svg</file>
<file>theme/black/caret-down.svg</file>
<file>theme/white/user.svg</file>
<file>theme/black/user.svg</file>
<file>theme/white/add.svg</file>
<file>theme/black/add.svg</file>
<file>theme/black/activity.svg</file>
<file>theme/black/bell.svg</file>
<file>theme/black/state-info.svg</file>
</qresource>
</RCC>

2
theme/black/activity.svg Normal file
View file

@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1"><path fill="#000" d="m32 2-20 36h22l-2 24 20-36h-22z"/>
</svg>

After

Width:  |  Height:  |  Size: 140 B

1
theme/black/add.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path fill="#000" d="M9.02 13.98h-2v-5h-5v-2h5v-5h2v5l5-.028V8.98h-5z"/></svg>

After

Width:  |  Height:  |  Size: 175 B

3
theme/black/bell.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16">
<path d="m8 2c-0.5523 0-1 0.4477-1 1 0 0.0472 0.021 0.0873 0.0273 0.1328-1.7366 0.4362-3.0273 1.9953-3.0273 3.8672v2l-1 1v1h10v-1l-1-1v-2c0-1.8719-1.291-3.431-3.0273-3.8672 0.0063-0.0455 0.0273-0.0856 0.0273-0.1328 0-0.5523-0.4477-1-1-1zm-2 10c0 1.1046 0.8954 2 2 2s2-0.8954 2-2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 400 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path d="M4 6l4 4 4-3.994z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 145 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4.2333 4.2333" version="1.1" height="16" width="16"><g id="g3830" transform="matrix(.87498 0 0 .87498 .26458 -255.9)"><circle id="circle3818" stroke-width=".25066" fill="#000" r="2.1167" cy="294.88" cx="2.1167" /><g id="g3828" stroke-linejoin="round" stroke-linecap="round" fill="none" /></g><path style="fill:#ffffff;stroke-width:0.17859235" d="m 1.6076619,2.1122981 c 0.027682,0.068222 0.058043,0.1232286 0.115014,0.043934 0.072686,-0.047862 0.314322,-0.2548509 0.29682,-0.061078 C 1.953774,2.4553739 1.8705497,2.8125586 1.8105428,3.1738508 1.7403561,3.3728027 1.9237704,3.5430012 2.1028984,3.4078068 2.295421,3.3181535 2.4582973,3.1779584 2.6256382,3.0488362 2.599921,2.9911507 2.5809903,2.9077482 2.5191973,2.9868644 2.4356161,3.0297263 2.2566665,3.2222491 2.2163047,3.07116 2.2725613,2.681829 2.3904322,2.3041062 2.4600833,1.9170966 2.5309844,1.7376113 2.3950755,1.5200858 2.210054,1.6736753 1.985742,1.7836882 1.8010774,1.9562083 1.6076619,2.1122981 Z M 2.4041839,0.77839186 C 2.1702279,0.77446305 2.0636081,1.1609366 2.2889917,1.2561264 2.4716917,1.3236342 2.659928,1.1286114 2.6086721,0.94358974 2.5911701,0.8467927 2.5018738,0.77035521 2.4038266,0.77749894 Z" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1
theme/black/user.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 16" height="16" width="16" version="1.1"><path d="m5 3.8c0 1.4 0.1 2.4 0.8 3.5 0.2 0.286 0.5 0.35 0.7 0.6 0.135 0.5 0.24 0.98 0.1 1.5-1.275 0.45-2.49 1-3.6 1.6-0.85 0.6-0.785 0.31-1 2.3-0.16 1.59 3.5 1.7 6 1.7s6.163-0.1 6-1.7c-0.215-2-0.23-1.71-1-2.3-1.1-0.654-2.45-1.167-3.6-1.6-0.15-0.56-0.04-0.973 0.1-1.5 0.235-0.25 0.5-0.363 0.7-0.6 0.69-0.885 0.8-2.425 0.8-3.5 0-1.59-1.43-2.8-3-2.8-1.75 0-3 1.43-3 2.8z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 486 B

1
theme/white/add.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path fill="#fff" d="M9.02 13.98h-2v-5h-5v-2h5v-5h2v5l5-.028V8.98h-5z"/></svg>

After

Width:  |  Height:  |  Size: 175 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewbox="0 0 16 16"><path d="M4 6l4 4 4-3.994z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 145 B

1
theme/white/folder.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" version="1.1" height="16"><path fill="#fff" d="m1.5 2c-0.25 0-0.5 0.25-0.5 0.5v11c0 0.26 0.24 0.5 0.5 0.5h13c0.26 0 0.5-0.241 0.5-0.5v-9c0-0.25-0.25-0.5-0.5-0.5h-6.5l-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 252 B

View file

@ -0,0 +1 @@
<svg width="32" height="32" enable-background="new 0 0 595.275 311.111" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.44643 0 0 .44643 260.1 -.096653)" stroke-width="2"><path d="m-572.71 3.5765c-1.108 0-2 0.892-2 2v4c0 1.108 0.892 2 2 2s2-0.892 2-2v-4c0-1.108-0.892-2-2-2zm16 0c-1.108 0-2 0.892-2 2v4c0 1.108 0.892 2 2 2s2-0.892 2-2v-4c0-1.108-0.892-2-2-2zm-13 4v2c0 1.662-1.338 3-3 3s-3-1.338-3-3v-1.875c-1.728 0.44254-3 2.0052-3 3.875v16c0 2.216 1.784 4 4 4h20c2.216 0 4-1.784 4-4v-16c0-1.8698-1.272-3.4325-3-3.875v1.875c0 1.662-1.338 3-3 3s-3-1.338-3-3v-2zm-5.9062 9h21.812c0.0554 0 0.0937 0.03835 0.0937 0.09375v11.812c0 0.0554-0.0384 0.09375-0.0937 0.09375h-21.812c-0.0554 0-0.0937-0.03835-0.0937-0.09375v-11.812c0-0.0554 0.0384-0.09375 0.0937-0.09375z" fill="#fff" stroke-width="4"/></g><g fill="#fff"><path d="m20.551 3.0408c-0.96 0-1.7744 0.70205-1.7744 1.608 0.0068 0.28636 0.03242 0.6395 0.20332 1.3862v0.0185l0.0185 0.01849c0.05485 0.15714 0.13469 0.24703 0.24028 0.36967s0.23147 0.26699 0.35117 0.38815c0.0141 0.01427 0.02311 0.02307 0.03695 0.03699 0.02376 0.10331 0.0525 0.21449 0.07393 0.31422 0.05703 0.26534 0.05118 0.45324 0.03695 0.5175-0.4125 0.14484-0.9257 0.31734-1.3862 0.5175-0.25853 0.1124-0.49247 0.21277-0.68385 0.33271-0.19138 0.11994-0.38172 0.21055-0.44358 0.48057-8.5e-4 0.01228-8.5e-4 0.02466 0 0.03699-0.06045 0.55505-0.1519 1.3713-0.2218 1.9223-0.01509 0.11598 0.04603 0.23822 0.14786 0.29574 0.8361 0.45164 2.1205 0.6334 3.4008 0.62845 1.2804-5e-3 2.5545-0.19746 3.3638-0.62845 0.10182-0.05751 0.16295-0.17976 0.14786-0.29574-0.02233-0.17222-0.04973-0.56055-0.07393-0.94265-0.02419-0.3821-0.04521-0.758-0.07393-0.97965-0.01-0.05495-0.036-0.10688-0.07393-0.14786-0.25712-0.30704-0.64125-0.49471-1.0905-0.6839-0.41012-0.1727-0.89095-0.35204-1.3677-0.5545-0.02669-0.05945-0.05319-0.23241 0-0.49906 0.01427-0.0716 0.03665-0.14828 0.05545-0.2218 0.04481-0.05018 0.07973-0.0912 0.12938-0.14787 0.10589-0.12086 0.21967-0.24765 0.3142-0.36967 0.09454-0.12202 0.17188-0.2267 0.2218-0.36967l0.01849-0.0185c0.19319-0.77975 0.1933-1.1051 0.20332-1.3862v-0.0185c0-0.906-0.81435-1.608-1.7744-1.608zm5.0755-1.4756c-1.3996 0-2.5869 1.0236-2.5869 2.3444 0.0099 0.41749 0.04727 0.93235 0.29642 2.021v0.02695l0.02694 0.02695c0.07998 0.22909 0.19637 0.36014 0.35031 0.53895 0.15394 0.1788 0.33747 0.38924 0.512 0.5659 0.02052 0.02078 0.03367 0.03367 0.05389 0.05391 0.03463 0.15061 0.07654 0.3127 0.10779 0.4581 0.08314 0.38683 0.07461 0.6608 0.0539 0.75455-0.6014 0.21117-1.3496 0.46264-2.021 0.75455-0.37693 0.16386-0.718 0.31019-0.99705 0.48505-0.27904 0.17486-0.55655 0.30697-0.64675 0.70065-0.0013 0.01794-0.0013 0.03596 0 0.05391-0.08814 0.80925-0.22146 1.9993-0.32336 2.8025-0.02199 0.16908 0.06712 0.34731 0.21558 0.43115 1.219 0.65845 3.0916 0.9235 4.9584 0.9162 1.8666-0.0073 3.7243-0.28787 4.9044-0.9162 0.14846-0.08384 0.23758-0.26207 0.21558-0.43115-0.03255-0.25107-0.0725-0.8172-0.10779-1.3743-0.03527-0.55705-0.06592-1.105-0.10778-1.4282-0.01461-0.0801-0.05248-0.15582-0.10779-0.21558-0.37486-0.44763-0.93495-0.72125-1.5899-0.99705-0.59795-0.25178-1.299-0.51325-1.994-0.8084-0.03889-0.08667-0.07756-0.33883 0-0.7276 0.02082-0.10438 0.05344-0.21619 0.08084-0.32336 0.06533-0.07317 0.11624-0.13296 0.18863-0.21558 0.15438-0.1762 0.32027-0.36105 0.4581-0.53895 0.13783-0.17789 0.2506-0.3305 0.32336-0.53895l0.02695-0.02694c0.2825-1.1368 0.2825-1.6111 0.297-2.021v-0.02695c0-1.3208-1.1874-2.3444-2.5869-2.3444z" color="#000000" style="text-indent:0;text-transform:none"/><path d="m1.7778 19.75c-0.4309 0-0.7778 0.3472-0.7778 0.77805v7.195c0 0.43094 0.34689 0.777 0.7778 0.777h12.444c0.431 0 0.778-0.346 0.778-0.777v-7.195c0-0.43105-0.347-0.77825-0.778-0.77825zm0.65625 0.8996 5.323 5.323h0.46183l5.347-5.323 0.5347 0.5347-3.184 3.2326 2.4062 2.4548-0.5347 0.5347-2.4548-2.4548-1.7744 1.7986h-1.1175l-1.774-1.798-2.455 2.479-0.53475-0.5595 2.4308-2.4545-3.2085-3.2326z"/><path d="m18.5 22.25a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5zm5.5 0a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5zm5.5 0a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 1 1 0-3.5z"/></g></svg>

After

Width:  |  Height:  |  Size: 4 KiB

1
theme/white/talk-app.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16"><path d="m7.9992 0.999a6.9993 6.9994 0 0 0 -6.9992 6.9996 6.9993 6.9994 0 0 0 6.9992 6.9994 6.9993 6.9994 0 0 0 3.6308 -1.024c0.86024 0.34184 2.7871 1.356 3.2457 0.91794 0.47922-0.45765-0.56261-2.6116-0.81238-3.412a6.9993 6.9994 0 0 0 0.935 -3.4814 6.9993 6.9994 0 0 0 -6.9991 -6.9993zm0.0008 2.6611a4.34 4.3401 0 0 1 4.34 4.3401 4.34 4.3401 0 0 1 -4.34 4.3398 4.34 4.3401 0 0 1 -4.34 -4.3398 4.34 4.3401 0 0 1 4.34 -4.3401z" stroke-width="0.14" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 563 B

1
theme/white/user.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 16" height="16" width="16" version="1.1"><path d="m5 3.8c0 1.4 0.1 2.4 0.8 3.5 0.2 0.286 0.5 0.35 0.7 0.6 0.135 0.5 0.24 0.98 0.1 1.5-1.275 0.45-2.49 1-3.6 1.6-0.85 0.6-0.785 0.31-1 2.3-0.16 1.59 3.5 1.7 6 1.7s6.163-0.1 6-1.7c-0.215-2-0.23-1.71-1-2.3-1.1-0.654-2.45-1.167-3.6-1.6-0.15-0.56-0.04-0.973 0.1-1.5 0.235-0.25 0.5-0.363 0.7-0.6 0.69-0.885 0.8-2.425 0.8-3.5 0-1.59-1.43-2.8-3-2.8-1.75 0-3 1.43-3 2.8z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 486 B