Merge pull request #491 from nextcloud/fix-activities-v2

Fix activities v2
This commit is contained in:
Roeland Jago Douma 2018-07-30 22:39:08 +02:00 committed by GitHub
commit fa5026bba7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1221 additions and 1920 deletions

View file

@ -22,6 +22,14 @@
<file>resources/account.png</file>
<file>resources/more.svg</file>
<file>resources/delete.png</file>
<file>resources/bell.png</file>
<file>resources/close.svg</file>
<file>resources/bell.svg</file>
<file>resources/link.svg</file>
<file>resources/files.svg</file>
<file>resources/folder-grey.png</file>
<file>resources/state-error.svg</file>
<file>resources/state-warning.svg</file>
<file>resources/folder.svg</file>
</qresource>
<qresource prefix="/"/>
</RCC>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 B

4
resources/bell.svg Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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: 456 B

4
resources/close.svg Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1">
<path d="m12.95 11.536l-1.414 1.414-3.536-3.536-3.535 3.536-1.415-1.414 3.536-3.536-3.536-3.536 1.415-1.414 3.535 3.536 3.516-3.555 1.434 1.434-3.536 3.535z"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

1
resources/files.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1"><path d="M1.457 1.997c-.25 0-.46.21-.46.46v11.08c0 .26.202.46.46.46h13.08c.258 0 .46-.2.46-.46V4.46c0-.25-.21-.463-.46-.463h-6.54l-2-2z" /></svg>

After

Width:  |  Height:  |  Size: 220 B

1
resources/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="#0082c9" 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: 255 B

1
resources/link.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewbox="0 0 16 16"><path d="M7.4 0C3.3 0 0 3.3 0 7.4s3.3 7.4 7.4 7.4 7.4-3.3 7.4-7.4S11.5 0 7.4 0zm.8.9c1.3 0 2.4.8 3.5 1.3l1.8 2.5-.3 1.1.6.3V8.5c-.2.7-.6 1.3-.9 2-.2.1 0-.8-.1-1 0-.6-.5-.6-.9-.2-.4.3-1.4.3-1.5-.4-.3-.8 0-1.7.3-2.5l-.6-.7.2-1.8-.8-.9.2-1-1-.6c-.2-.2-.6-.2-.7-.4.1 0 .2-.1.2-.1zM5.6 1s.1 0 .1.1c.4.2-.1.4-.2.6-.5.3.3.7.5 1 .4-.1.8-.7 1.4-.5.7-.2.6.6 1.1 1 .1.2.9.8.4.6-.5-.4-1-.4-1.3.1-.8.5-.3-.9-.7-1.2-.6-.7-.4.5-.4.9-.4 0-1.1-.3-1.5.2l.4.6.5-.7c0-.3.1.2.3.3.1.2.8.7.3.9-.8.4-1.4 1.1-2.1 1.7-.2.5-.7.4-1 0-.7-.4-.7.7-.6 1.1l.6-.4v1.1c-.4.4-.9-.7-1.3-.9V5.9c0-.4-.1-.9 0-1.3.8-.9 1.7-1.9 2.2-3h.8c.6.2.3-.7.5-.6zM4.4 9.2c.1 0 .2 0 .3.1.8.1 1.4.7 2 1.1.5.5 1.6.3 1.7 1.2-.2.9-1.1 1.4-1.8 1.7-.2.1-.4.2-.6.2-.7.2-1-.6-1.2-1.1-.3-.7-1.1-1.2-1-2.1 0-.4.2-1 .6-1.1z"/></svg>

After

Width:  |  Height:  |  Size: 851 B

View file

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 4.2333 4.2333" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.87498 0 0 .87498 .26458 -255.9)"><circle cx="2.1167" cy="294.88" r="2.1167" fill="#d40000" stroke-width=".25066"/><path d="m1.1839 293.95 1.8656 1.8656" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width=".52917"/><path d="m3.0495 293.95-1.8656 1.8656" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width=".52917"/></g></svg>

After

Width:  |  Height:  |  Size: 522 B

View file

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 4.2333 4.2333" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.87498 0 0 .87498 .26458 -255.9)"><circle cx="2.1167" cy="294.88" r="2.1167" fill="#e0d92d" stroke-width=".25066"/><path d="m2.1167 293.83v1.3156" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width=".52917"/><circle cx="2.1103" cy="295.95" r=".33073" fill="#fcffff"/></g></svg>

After

Width:  |  Height:  |  Size: 439 B

View file

@ -22,8 +22,6 @@ set(client_UI_SRCS
generalsettings.ui
ignorelisteditor.ui
networksettings.ui
protocolwidget.ui
issueswidget.ui
activitywidget.ui
synclogdialog.ui
settingsdialog.ui
@ -70,8 +68,6 @@ set(client_SRCS
openfilemanager.cpp
owncloudgui.cpp
owncloudsetupwizard.cpp
protocolwidget.cpp
issueswidget.cpp
activitydata.cpp
activitylistmodel.cpp
activitywidget.cpp
@ -102,6 +98,7 @@ set(client_SRCS
servernotificationhandler.cpp
guiutility.cpp
elidedlabel.cpp
iconjob.cpp
creds/credentialsfactory.cpp
creds/httpcredentialsgui.cpp
creds/oauth.cpp

View file

@ -16,6 +16,7 @@
#define ACTIVITYDATA_H
#include <QtCore>
#include <QIcon>
namespace OCC {
/**
@ -48,18 +49,25 @@ public:
enum Type {
ActivityType,
NotificationType
NotificationType,
SyncResultType,
SyncFileItemType
};
Type _type;
qlonglong _id;
QString _objectType;
QString _subject;
QString _message;
QString _folder;
QString _file;
QUrl _link;
QDateTime _dateTime;
QString _accName;
// Stores information about the error
int _status;
QVector<ActivityLink> _links;
/**
* @brief Sort operator to sort the list youngest first.
@ -85,4 +93,7 @@ bool operator<(const Activity &rhs, const Activity &lhs);
typedef QList<Activity> ActivityList;
}
Q_DECLARE_METATYPE(OCC::Activity::Type)
Q_DECLARE_METATYPE(OCC::ActivityLink)
#endif // ACTIVITYDATA_H

View file

@ -18,6 +18,7 @@
#include "folderstatusmodel.h"
#include "folderman.h"
#include "accountstate.h"
#include "activitydata.h"
#include <theme.h>
#include <account.h>
@ -29,6 +30,13 @@ 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");
int ActivityItemDelegate::iconHeight()
{
@ -51,9 +59,9 @@ int ActivityItemDelegate::rowHeight()
QFont f = opt.font;
QFontMetrics fm(f);
_margin = fm.height() / 4;
_margin = fm.height() / 2;
}
return iconHeight() + 2 * _margin;
return iconHeight() + 5 * _margin;
}
QSize ActivityItemDelegate::sizeHint(const QStyleOptionViewItem &option,
@ -68,95 +76,246 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
const QModelIndex &index) const
{
QStyledItemDelegate::paint(painter, option, index);
QFont font = option.font;
QFontMetrics fm(font);
int margin = fm.height() / 4;
int margin = fm.height() / 2.5;
painter->save();
int iconSize = 16;
int iconOffset = qRound(fm.height() / 4.0 * 7.0);
int offset = 4;
// get the data
Activity::Type activityType = qvariant_cast<Activity::Type>(index.data(ActionRole));
QIcon actionIcon = qvariant_cast<QIcon>(index.data(ActionIconRole));
QIcon userIcon = qvariant_cast<QIcon>(index.data(UserIconRole));
QString objectType = qvariant_cast<QString>(index.data(ObjectTypeRole));
QString actionText = qvariant_cast<QString>(index.data(ActionTextRole));
QString pathText = qvariant_cast<QString>(index.data(PathRole));
QString remoteLink = qvariant_cast<QString>(index.data(LinkRole));
QString messageText = qvariant_cast<QString>(index.data(MessageRole));
QList<QVariant> customList = index.data(ActionsLinksRole).toList();
QString timeText = qvariant_cast<QString>(index.data(PointInTimeRole));
QString accountRole = qvariant_cast<QString>(index.data(AccountRole));
bool accountOnline = qvariant_cast<bool>(index.data(AccountConnectedRole));
// activity/notification icons
QRect actionIconRect = option.rect;
QRect userIconRect = 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));
int iconHeight = qRound(fm.height() / 5.0 * 8.0);
int iconWidth = iconHeight;
// subject text rect
QRect actionTextBox = actionIconRect;
int actionTextBoxWidth = fm.width(actionText);
actionTextBox.setTop(option.rect.top() + margin + offset/2);
actionTextBox.setHeight(fm.height());
actionTextBox.setLeft(actionIconRect.right() + margin);
actionTextBox.setRight(actionTextBox.left() + actionTextBoxWidth + margin);
actionIconRect.setLeft(option.rect.left() + margin);
actionIconRect.setWidth(iconWidth);
actionIconRect.setHeight(iconHeight);
actionIconRect.setTop(actionIconRect.top() + margin);
userIconRect.setLeft(actionIconRect.right() + margin);
userIconRect.setWidth(iconWidth);
userIconRect.setHeight(iconHeight);
userIconRect.setTop(actionIconRect.top());
// message text rect
QRect messageTextBox = actionTextBox;
int messageTextWidth = fm.width(messageText);
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());
}
int textTopOffset = qRound((iconHeight - fm.height()) / 2.0);
// time rect
QRect timeBox;
int timeBoxWidth = fm.boundingRect(QLatin1String("4 hour(s) ago on longlongdomain.org")).width(); // FIXME.
timeBox.setTop(actionIconRect.top() + textTopOffset);
timeBox.setLeft(option.rect.right() - timeBoxWidth - margin);
timeBox.setWidth(timeBoxWidth);
// time box rect
QRect timeBox = messageTextBox;
QString timeStr = tr("%1").arg(timeText);
int timeTextWidth = fm.width(timeStr);
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());
timeBox.setRight(timeBox.left() + timeTextWidth + margin);
QRect actionTextBox = timeBox;
actionTextBox.setLeft(userIconRect.right() + margin);
actionTextBox.setRight(timeBox.left() - 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;
/* === start drawing === */
QPixmap pm = actionIcon.pixmap(iconWidth, iconHeight, QIcon::Normal);
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 = QIcon(QLatin1String(":/client/resources/close.svg"));
if(customList.size() > 1)
secondaryButton.icon = QIcon(QLatin1String(":/client/resources/more.svg"));
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");
primaryButton.rect.setLeft(left - margin * 2 - fm.width(primaryButton.text));
// 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 = QIcon(QLatin1String(":/client/resources/folder.svg"));
secondaryButton.iconSize = QSize(iconSize, iconSize);
// Primary button will be 'open browser'
primaryButton.text = tr("Open Browser");
primaryButton.rect.setLeft(left - margin * 2 - fm.width(primaryButton.text));
// 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 = QIcon(QLatin1String(":/client/resources/folder.svg"));
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);
pm = userIcon.pixmap(iconWidth, iconHeight, QIcon::Normal);
painter->drawPixmap(QPoint(userIconRect.left(), userIconRect.top()), pm);
// change pen color if use is not online
QPalette p = option.palette;
if(!accountOnline)
painter->setPen(p.color(QPalette::Disabled, QPalette::Text));
// change pen color if the line is selected
QPalette::ColorGroup cg = option.state & QStyle::State_Enabled
? QPalette::Normal
: QPalette::Disabled;
if (cg == QPalette::Normal && !(option.state & QStyle::State_Active))
cg = QPalette::Inactive;
if (option.state & QStyle::State_Selected) {
painter->setPen(option.palette.color(cg, QPalette::HighlightedText));
} else {
painter->setPen(option.palette.color(cg, QPalette::Text));
}
const QString elidedAction = fm.elidedText(actionText, Qt::ElideRight, actionTextBox.width());
if (option.state & QStyle::State_Selected)
painter->setPen(option.palette.color(cg, QPalette::HighlightedText));
else
painter->setPen(option.palette.color(cg, 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);
int atPos = accountRole.indexOf(QLatin1Char('@'));
if (atPos > -1) {
accountRole.remove(0, atPos + 1);
// draw the buttons
if(activityType == Activity::Type::NotificationType || activityType == Activity::Type::SyncResultType)
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()){
painter->setPen(p.color(QPalette::Inactive, QPalette::Text));
// check if line is selected
if (option.state & QStyle::State_Selected)
painter->setPen(option.palette.color(cg, QPalette::HighlightedText));
const QString elidedMessage = fm.elidedText(messageText, Qt::ElideRight, spaceLeftForText);
painter->drawText(messageTextBox, elidedMessage);
}
QString timeStr;
if (accountOnline) {
timeStr = tr("%1 on %2").arg(timeText, accountRole);
} else {
timeStr = tr("%1 on %2 (disconnected)").arg(timeText, accountRole);
QPalette p = option.palette;
painter->setPen(p.color(QPalette::Disabled, QPalette::Text));
}
const QString elidedTime = fm.elidedText(timeStr, Qt::ElideRight, timeBox.width());
// change pen color for the time
painter->setPen(p.color(QPalette::Disabled, QPalette::Text));
// check if line is selected
if (option.state & QStyle::State_Selected)
painter->setPen(option.palette.color(cg, QPalette::HighlightedText));
// draw the time
const QString elidedTime = fm.elidedText(timeStr, 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);
}

View file

@ -15,6 +15,9 @@
#pragma once
#include <QStyledItemDelegate>
#include <QMouseEvent>
class QMouseEvent;
namespace OCC {
@ -29,11 +32,16 @@ public:
enum datarole { ActionIconRole = Qt::UserRole + 1,
UserIconRole,
AccountRole,
ObjectTypeRole,
ActionsLinksRole,
ActionTextRole,
ActionRole,
MessageRole,
PathRole,
LinkRole,
PointInTimeRole,
AccountConnectedRole };
AccountConnectedRole,
SyncFileStatusRole };
void paint(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const Q_DECL_OVERRIDE;
QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const Q_DECL_OVERRIDE;
@ -43,9 +51,20 @@ public:
static int rowHeight();
static int iconHeight();
signals:
void primaryButtonClickedOnItemView(const QModelIndex &index);
void secondaryButtonClickedOnItemView(const QModelIndex &index);
private:
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;
};
} // namespace OCC

View file

@ -29,12 +29,17 @@
#include "activitydata.h"
#include "activitylistmodel.h"
#include "theme.h"
#include "servernotificationhandler.h"
namespace OCC {
Q_LOGGING_CATEGORY(lcActivity, "nextcloud.gui.activity", QtInfoMsg)
ActivityListModel::ActivityListModel(QWidget *parent)
ActivityListModel::ActivityListModel(AccountState *accountState, QWidget *parent)
: QAbstractListModel(parent)
, _accountState(accountState)
{
}
@ -42,40 +47,87 @@ 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();
a = _finalList.at(index.row());
AccountStatePtr ast = AccountManager::instance()->account(a._accName);
if (!ast)
if (!ast && _accountState != ast.data())
return QVariant();
QStringList list;
switch (role) {
case ActivityItemDelegate::PathRole:
if(!a._file.isEmpty()){
list = FolderMan::instance()->findFileInLocalFolders(a._file, ast->account());
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) {
return QVariant(list.at(0));
}
// File does not exist anymore? Let's try to open its path
list = FolderMan::instance()->findFileInLocalFolders(QFileInfo(a._file).path(), ast->account());
list = FolderMan::instance()->findFileInLocalFolders(QFileInfo(relPath).path(), ast->account());
if (list.count() > 0) {
return QVariant(list.at(0));
}
}
return QVariant();
break;
case ActivityItemDelegate::ActionsLinksRole:{
QList<QVariant> customList;
foreach (ActivityLink customItem, a._links) {
QVariant customVariant;
customVariant.setValue(customItem);
customList << customVariant;
}
return customList;
break;
}
case ActivityItemDelegate::ActionIconRole:
return QVariant(); // FIXME once the action can be quantified, display on Icon
if(a._type == Activity::NotificationType){
QIcon cachedIcon = ServerNotificationHandler::iconCache.value(a._id);
if(!cachedIcon.isNull())
return cachedIcon;
else return QIcon(QLatin1String(":/client/resources/bell.svg"));
} else if(a._type == Activity::SyncResultType){
return QIcon(QLatin1String(":/client/resources/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 QIcon(QLatin1String(":/client/resources/state-error.svg"));
} else if(a._status == SyncFileItem::SoftError
|| a._status == SyncFileItem::FileIgnored
|| a._status == SyncFileItem::Conflict
|| a._status == SyncFileItem::Restoration){
return QIcon(QLatin1String(":/client/resources/state-warning.svg"));
}
return QIcon(QLatin1String(":/client/resources/activity.png"));
} else return QIcon(QLatin1String(":/client/resources/activity.png"));
return QVariant();
break;
case ActivityItemDelegate::UserIconRole:
return QIcon(QLatin1String(":/client/resources/account.png"));
case ActivityItemDelegate::ObjectTypeRole:
return a._objectType;
break;
case ActivityItemDelegate::ActionRole:{
QVariant type;
type.setValue(a._type);
return type;
break;
}
case Qt::ToolTipRole:
case ActivityItemDelegate::ActionTextRole:
return a._subject;
break;
case ActivityItemDelegate::MessageRole:
return a._message;
break;
case ActivityItemDelegate::LinkRole:
return a._link;
break;
@ -107,36 +159,31 @@ bool ActivityListModel::canFetchMore(const QModelIndex &) const
if (_activityLists.count() == 0)
return true;
for (auto i = _activityLists.begin(); i != _activityLists.end(); ++i) {
AccountState *ast = i.key();
if (ast && ast->isConnected()) {
ActivityList activities = i.value();
if (activities.count() == 0 && !_currentlyFetching.contains(ast)) {
return true;
}
if (_accountState && _accountState->isConnected()) {
if (_activityLists.count() == 0 && !_currentlyFetching) {
return true;
}
}
return false;
}
void ActivityListModel::startFetchJob(AccountState *s)
void ActivityListModel::startFetchJob()
{
if (!s->isConnected()) {
if (!_accountState->isConnected()) {
return;
}
JsonApiJob *job = new JsonApiJob(s->account(), QLatin1String("ocs/v1.php/cloud/activity"), this);
JsonApiJob *job = new JsonApiJob(_accountState->account(), QLatin1String("ocs/v2.php/cloud/activity"), this);
QObject::connect(job, &JsonApiJob::jsonReceived,
this, &ActivityListModel::slotActivitiesReceived);
job->setProperty("AccountStatePtr", QVariant::fromValue<QPointer<AccountState>>(s));
QUrlQuery params;
params.addQueryItem(QLatin1String("page"), QLatin1String("0"));
params.addQueryItem(QLatin1String("pagesize"), QLatin1String("100"));
job->addQueryParams(params);
_currentlyFetching.insert(s);
qCInfo(lcActivity) << "Start fetching activities for " << s->account()->displayName();
_currentlyFetching = true;
qCInfo(lcActivity) << "Start fetching activities for " << _accountState->account()->displayName();
job->start();
}
@ -145,11 +192,11 @@ void ActivityListModel::slotActivitiesReceived(const QJsonDocument &json, int st
auto activities = json.object().value("ocs").toObject().value("data").toArray();
ActivityList list;
auto ast = qvariant_cast<QPointer<AccountState>>(sender()->property("AccountStatePtr"));
auto ast = _accountState;
if (!ast)
return;
_currentlyFetching.remove(ast);
_currentlyFetching = 0;
foreach (auto activ, activities) {
auto json = activ.toObject();
@ -166,23 +213,66 @@ void ActivityListModel::slotActivitiesReceived(const QJsonDocument &json, int st
list.append(a);
}
_activityLists[ast] = list;
_activityLists = list;
emit activityJobStatusCode(ast, statusCode);
emit activityJobStatusCode(statusCode);
combineActivityLists();
}
void ActivityListModel::addErrorToActivityList(Activity activity) {
qCInfo(lcActivity) << "Error successfully added to the notification list: " << activity._subject;
_notificationErrorsLists.prepend(activity);
combineActivityLists();
}
void ActivityListModel::addNotificationToActivityList(Activity activity) {
qCInfo(lcActivity) << "Notification successfully added to the notification list: " << activity._subject;
_notificationLists.prepend(activity);
combineActivityLists();
}
void ActivityListModel::removeActivityFromActivityList(int row) {
Activity activity = _finalList.at(row);
removeActivityFromActivityList(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){
index = _activityLists.indexOf(activity);
if(index != -1) _activityLists.removeAt(index);
} else if(activity._type == Activity::NotificationType){
index = _notificationLists.indexOf(activity);
if(index != -1) _notificationLists.removeAt(index);
} else {
index = _notificationErrorsLists.indexOf(activity);
if(index != -1) _notificationErrorsLists.removeAt(index);
}
if(index != -1){
qCInfo(lcActivity) << "Activity/Notification/Error successfully removed from the list.";
qCInfo(lcActivity) << "Updating Activity/Notification/Error view.";
combineActivityLists();
}
}
void ActivityListModel::combineActivityLists()
{
ActivityList resultList;
foreach (ActivityList list, _activityLists.values()) {
resultList.append(list);
}
std::sort(_notificationErrorsLists.begin(), _notificationErrorsLists.end());
resultList.append(_notificationErrorsLists);
std::sort(resultList.begin(), resultList.end());
std::sort(_notificationLists.begin(), _notificationLists.end());
resultList.append(_notificationLists);
std::sort(_activityLists.begin(), _activityLists.end());
resultList.append(_activityLists);
beginResetModel();
_finalList.clear();
@ -195,42 +285,22 @@ void ActivityListModel::combineActivityLists()
void ActivityListModel::fetchMore(const QModelIndex &)
{
QList<AccountStatePtr> accounts = AccountManager::instance()->accounts();
foreach (const AccountStatePtr &asp, accounts) {
if (!_activityLists.contains(asp.data()) && asp->isConnected()) {
_activityLists[asp.data()] = ActivityList();
startFetchJob(asp.data());
}
if (_accountState->isConnected()) {
_activityLists = ActivityList();
startFetchJob();
}
}
void ActivityListModel::slotRefreshActivity(AccountState *ast)
void ActivityListModel::slotRefreshActivity()
{
if (ast && _activityLists.contains(ast)) {
_activityLists.remove(ast);
}
startFetchJob(ast);
_activityLists.clear();
startFetchJob();
}
void ActivityListModel::slotRemoveAccount(AccountState *ast)
void ActivityListModel::slotRemoveAccount()
{
if (_activityLists.contains(ast)) {
int i = 0;
const QString accountToRemove = ast->account()->displayName();
QMutableListIterator<Activity> it(_finalList);
while (it.hasNext()) {
Activity activity = it.next();
if (activity._accName == accountToRemove) {
beginRemoveRows(QModelIndex(), i, i + 1);
it.remove();
endRemoveRows();
}
}
_activityLists.remove(ast);
_currentlyFetching.remove(ast);
}
_finalList.clear();
_activityLists.clear();
_currentlyFetching = false;
}
}

View file

@ -38,7 +38,7 @@ class ActivityListModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit ActivityListModel(QWidget *parent = 0);
explicit ActivityListModel(AccountState *accountState, QWidget *parent = 0);
QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE;
int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
@ -47,24 +47,32 @@ public:
void fetchMore(const QModelIndex &) Q_DECL_OVERRIDE;
ActivityList activityList() { return _finalList; }
ActivityList errorsList() { return _notificationErrorsLists; }
void addNotificationToActivityList(Activity activity);
void addErrorToActivityList(Activity activity);
void removeActivityFromActivityList(int row);
void removeActivityFromActivityList(Activity activity);
public slots:
void slotRefreshActivity(AccountState *ast);
void slotRemoveAccount(AccountState *ast);
void slotRefreshActivity();
void slotRemoveAccount();
private slots:
void slotActivitiesReceived(const QJsonDocument &json, int statusCode);
signals:
void activityJobStatusCode(AccountState *ast, int statusCode);
void activityJobStatusCode(int statusCode);
private:
void startFetchJob(AccountState *s);
void startFetchJob();
void combineActivityLists();
QMap<AccountState *, ActivityList> _activityLists;
ActivityList _activityLists;
ActivityList _notificationLists;
ActivityList _notificationErrorsLists;
ActivityList _finalList;
QSet<AccountState *> _currentlyFetching;
AccountState *_accountState;
bool _currentlyFetching = true;
};
}
#endif // ACTIVITYLISTMODEL_H

View file

@ -29,8 +29,6 @@
#include "accountstate.h"
#include "accountmanager.h"
#include "activityitemdelegate.h"
#include "protocolwidget.h"
#include "issueswidget.h"
#include "QProgressIndicator.h"
#include "notificationwidget.h"
#include "notificationconfirmjob.h"
@ -38,8 +36,10 @@
#include "theme.h"
#include "ocsjob.h"
#include "configfile.h"
#include "guiutility.h"
#include "socketapi.h"
#include "ui_activitywidget.h"
#include "syncengine.h"
#include <climits>
@ -49,10 +49,13 @@
namespace OCC {
ActivityWidget::ActivityWidget(QWidget *parent)
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);
@ -61,39 +64,36 @@ ActivityWidget::ActivityWidget(QWidget *parent)
_ui->_activityList->setMinimumWidth(400);
#endif
_model = new ActivityListModel(this);
_model = new ActivityListModel(accountState, this);
ActivityItemDelegate *delegate = new ActivityItemDelegate;
delegate->setParent(this);
_ui->_activityList->setItemDelegate(delegate);
_ui->_activityList->setBackgroundRole(QPalette::Background);
_ui->_activityList->setAlternatingRowColors(true);
_ui->_activityList->setModel(_model);
_ui->_notifyLabel->hide();
_ui->_notifyScroll->hide();
// Create a widget container for the notifications. The ui file defines
// a scroll area that get a widget with a layout as children
QWidget *w = new QWidget;
_notificationsLayout = new QVBoxLayout;
w->setLayout(_notificationsLayout);
_notificationsLayout->setAlignment(Qt::AlignTop);
_ui->_notifyScroll->setAlignment(Qt::AlignTop);
_ui->_notifyScroll->setWidget(w);
showLabels();
connect(_model, &ActivityListModel::activityJobStatusCode,
this, &ActivityWidget::slotAccountActivityStatus);
_copyBtn = _ui->_dialogButtonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole);
_copyBtn->setToolTip(tr("Copy the activity list to the clipboard."));
connect(_copyBtn, &QAbstractButton::clicked, this, &ActivityWidget::copyToClipboard);
_ui->_copyButton->setToolTip(tr("Copy the activity list to the clipboard."));
connect(_ui->_copyButton, &QPushButton::click, this, &ActivityWidget::copyToClipboard);
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(&_removeTimer, &QTimer::timeout, this, &ActivityWidget::slotCheckToCleanWidgets);
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);
}
@ -102,39 +102,231 @@ ActivityWidget::~ActivityWidget()
delete _ui;
}
void ActivityWidget::slotRefreshActivities(AccountState *ptr)
void ActivityWidget::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
{
_model->slotRefreshActivity(ptr);
// TODO: this is really not working
// if (progress.status() == ProgressInfo::Done
// || 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::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::slotRefreshNotifications(AccountState *ptr)
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;
activity._status = item->_status;
activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate);
activity._subject = item->_errorString;
activity._message = item->_originalFile;
activity._link = folderInstance->accountState()->account()->url();
activity._accName = folderInstance->accountState()->account()->displayName();
activity._file = item->_file;
activity._folder = folder;
// 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;
ServerNotificationHandler *snh = new ServerNotificationHandler(_accountState);
connect(snh, &ServerNotificationHandler::newNotificationList,
this, &ActivityWidget::slotBuildNotificationDisplay);
snh->slotFetchNotifications(ptr);
snh->slotFetchNotifications();
} else {
qCWarning(lcActivity) << "Notification request counter not zero.";
}
}
void ActivityWidget::slotRemoveAccount(AccountState *ptr)
void ActivityWidget::slotRemoveAccount()
{
_model->slotRemoveAccount(ptr);
_model->slotRemoveAccount();
}
void ActivityWidget::showLabels()
{
QString t = tr("Server Activities");
_ui->_headerLabel->setTextFormat(Qt::RichText);
_ui->_headerLabel->setText(t);
_ui->_notifyLabel->setText(tr("Action Required: Notifications"));
t.clear();
QSetIterator<QString> i(_accountsWithoutActivities);
while (i.hasNext()) {
@ -144,15 +336,15 @@ void ActivityWidget::showLabels()
_ui->_bottomLabel->setText(t);
}
void ActivityWidget::slotAccountActivityStatus(AccountState *ast, int statusCode)
void ActivityWidget::slotAccountActivityStatus(int statusCode)
{
if (!(ast && ast->account())) {
if (!(_accountState && _accountState->account())) {
return;
}
if (statusCode == 999) {
_accountsWithoutActivities.insert(ast->account()->displayName());
_accountsWithoutActivities.insert(_accountState->account()->displayName());
} else {
_accountsWithoutActivities.remove(ast->account()->displayName());
_accountsWithoutActivities.remove(_accountState->account()->displayName());
}
checkActivityTabVisibility();
@ -176,35 +368,33 @@ void ActivityWidget::storeActivityList(QTextStream &ts)
foreach (Activity activity, activities) {
ts << right
// account name
<< qSetFieldWidth(30)
<< qSetFieldWidth(activity._accName.length())
<< activity._accName
// separator
<< qSetFieldWidth(0) << ","
<< qSetFieldWidth(2) << " - "
// date and time
<< qSetFieldWidth(34)
<< qSetFieldWidth(activity._dateTime.toString().length())
<< activity._dateTime.toString()
// separator
<< qSetFieldWidth(0) << ","
<< qSetFieldWidth(2) << " - "
// file
<< qSetFieldWidth(30)
// fileq
<< qSetFieldWidth(activity._file.length())
<< activity._file
// separator
<< qSetFieldWidth(0) << ","
<< qSetFieldWidth(2) << " - "
// subject
<< qSetFieldWidth(100)
<< qSetFieldWidth(activity._subject.length())
<< activity._subject
// separator
<< qSetFieldWidth(0) << ","
<< qSetFieldWidth(2) << " - "
// message (mostly empty)
<< qSetFieldWidth(55)
<< activity._message
//
<< qSetFieldWidth(0)
<< endl;
// message
<< qSetFieldWidth(activity._message.length())
<< activity._message
<< endl;
}
}
@ -215,12 +405,8 @@ void ActivityWidget::checkActivityTabVisibility()
_accountsWithoutActivities.count() != accountCount;
bool hasNotifications = !_widgetForNotifId.isEmpty();
_ui->_headerLabel->setVisible(hasAccountsWithActivity);
_ui->_activityList->setVisible(hasAccountsWithActivity);
_ui->_notifyLabel->setVisible(hasNotifications);
_ui->_notifyScroll->setVisible(hasNotifications);
emit hideActivityTab(!hasAccountsWithActivity && !hasNotifications);
}
@ -229,9 +415,10 @@ 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 (QFile::exists(fullPath)) {
showInFileManager(fullPath);
if(!fullPath.isEmpty()){
if (QFile::exists(fullPath)) {
showInFileManager(fullPath);
}
}
}
}
@ -242,10 +429,9 @@ void ActivityWidget::slotOpenFile(QModelIndex indx)
// collected.
void ActivityWidget::slotBuildNotificationDisplay(const ActivityList &list)
{
QHash<QString, int> accNotified;
QString listAccountName;
// Whether a new notification widget was added to the notificationLayout.
// Whether a new notification was added to the list
bool newNotificationShown = false;
foreach (auto activity, list) {
@ -254,26 +440,6 @@ void ActivityWidget::slotBuildNotificationDisplay(const ActivityList &list)
continue;
}
NotificationWidget *widget = 0;
if (_widgetForNotifId.contains(activity.ident())) {
widget = _widgetForNotifId[activity.ident()];
} else {
widget = new NotificationWidget(this);
connect(widget, &NotificationWidget::sendNotificationRequest,
this, &ActivityWidget::slotSendNotificationRequest);
connect(widget, &NotificationWidget::requestCleanupAndBlacklist,
this, &ActivityWidget::slotRequestCleanupAndBlacklist);
_notificationsLayout->addWidget(widget);
// _ui->_notifyScroll->setMinimumHeight( widget->height());
_ui->_notifyScroll->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow);
_widgetForNotifId[activity.ident()] = widget;
newNotificationShown = true;
}
widget->setActivity(activity);
// remember the list account name for the strayCat handling below.
listAccountName = activity._accName;
@ -287,22 +453,9 @@ void ActivityWidget::slotBuildNotificationDisplay(const ActivityList &list)
if (_guiLogTimer.elapsed() > 60 * 60 * 1000) {
_guiLoggedNotifications.clear();
}
if (!_guiLoggedNotifications.contains(activity._id)) {
QString host = activity._accName;
// store the name of the account that sends the notification to be
// able to add it to the tray notification
// remove the user name from the account as that is not accurate here.
int indx = host.indexOf(QChar('@'));
if (indx > -1) {
host.remove(0, 1 + indx);
}
if (!host.isEmpty()) {
if (accNotified.contains(host)) {
accNotified[host] = accNotified[host] + 1;
} else {
accNotified[host] = 1;
}
}
newNotificationShown = true;
_guiLoggedNotifications.insert(activity._id);
// Assemble a tray notification for the NEW notification
@ -314,53 +467,19 @@ void ActivityWidget::slotBuildNotificationDisplay(const ActivityList &list)
emit guiLog(activity._subject, activity._accName);
}
}
_model->addNotificationToActivityList(activity);
}
}
// check if there are widgets that have no corresponding activity from
// the server any more. Collect them in a list
QList<Activity::Identifier> strayCats;
foreach (auto id, _widgetForNotifId.keys()) {
NotificationWidget *widget = _widgetForNotifId[id];
bool found = false;
// do not mark widgets of other accounts to delete.
if (widget->activity()._accName != listAccountName) {
continue;
}
foreach (auto activity, list) {
if (activity.ident() == id) {
// found an activity
found = true;
break;
}
}
if (!found) {
// the activity does not exist any more.
strayCats.append(id);
}
}
// .. and now delete all these stray cat widgets.
foreach (auto strayCatId, strayCats) {
NotificationWidget *widgetToGo = _widgetForNotifId[strayCatId];
scheduleWidgetToRemove(widgetToGo, 0);
}
checkActivityTabVisibility();
if (newNotificationShown) {
// restart the gui log timer now that we show a notification
// restart the gui log timer now that we show a new notification
if(newNotificationShown)
_guiLogTimer.start();
emit newNotification();
}
}
void ActivityWidget::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb)
void ActivityWidget::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row)
{
qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName;
NotificationWidget *theSender = qobject_cast<NotificationWidget *>(sender());
const QStringList validVerbs = QStringList() << "GET"
<< "PUT"
@ -373,7 +492,7 @@ void ActivityWidget::slotSendNotificationRequest(const QString &accountName, con
NotificationConfirmJob *job = new NotificationConfirmJob(acc->account());
QUrl l(link);
job->setLinkAndVerb(l, verb);
job->setWidget(theSender);
job->setProperty("activityRow", QVariant::fromValue(row));
connect(job, &AbstractNetworkJob::networkError,
this, &ActivityWidget::slotNotifyNetworkError);
connect(job, &NotificationConfirmJob::jobFinished,
@ -392,9 +511,7 @@ void ActivityWidget::slotSendNotificationRequest(const QString &accountName, con
void ActivityWidget::endNotificationRequest(NotificationWidget *widget, int replyCode)
{
_notificationRequestsRunning--;
if (widget) {
widget->slotNotificationRequestFinished(replyCode);
}
slotNotificationRequestFinished(replyCode);
}
void ActivityWidget::slotNotifyNetworkError(QNetworkReply *reply)
@ -425,7 +542,7 @@ void ActivityWidget::slotNotifyServerFinished(const QString &reply, int replyCod
// Add 200 millisecs to the predefined value to make sure that the timer in
// widget's method readyToClose() has elapsed.
if (replyCode == OCS_SUCCESS_STATUS_CODE || replyCode == OCS_SUCCESS_STATUS_CODE_V2) {
scheduleWidgetToRemove(job->widget());
//scheduleWidgetToRemove(job->widget());
}
}
@ -480,57 +597,42 @@ void ActivityWidget::slotCheckToCleanWidgets()
if (_widgetsToRemove.isEmpty()) {
_removeTimer.stop();
}
// check to see if the whole notification pane should be hidden
if (_widgetForNotifId.isEmpty()) {
_ui->_notifyLabel->setHidden(true);
_ui->_notifyScroll->setHidden(true);
}
}
/* ==================================================================== */
ActivitySettings::ActivitySettings(QWidget *parent)
ActivitySettings::ActivitySettings(AccountState *accountState, QWidget *parent)
: QWidget(parent)
, _accountState(accountState)
{
QHBoxLayout *hbox = new QHBoxLayout(this);
setLayout(hbox);
_vbox = new QVBoxLayout(this);
setLayout(_vbox);
// create a tab widget for the three activity views
_tab = new QTabWidget(this);
hbox->addWidget(_tab);
_activityWidget = new ActivityWidget(this);
_activityTabId = _tab->addTab(_activityWidget, Theme::instance()->applicationIcon(), tr("Server Activity"));
_activityWidget = new ActivityWidget(_accountState, this);
// set background white
QPalette palette;
palette.setColor(QPalette::Background, Qt::white);
_activityWidget->setAutoFillBackground(true);
_activityWidget->setPalette(palette);
_vbox->insertWidget(1, _activityWidget);
connect(_activityWidget, &ActivityWidget::copyToClipboard, this, &ActivitySettings::slotCopyToClipboard);
connect(_activityWidget, &ActivityWidget::hideActivityTab, this, &ActivitySettings::setActivityTabHidden);
connect(_activityWidget, &ActivityWidget::guiLog, this, &ActivitySettings::guiLog);
connect(_activityWidget, &ActivityWidget::newNotification, this, &ActivitySettings::slotShowActivityTab);
_protocolWidget = new ProtocolWidget(this);
_protocolTabId = _tab->addTab(_protocolWidget, Theme::instance()->syncStateIcon(SyncResult::Success), tr("Sync Protocol"));
connect(_protocolWidget, &ProtocolWidget::copyToClipboard, this, &ActivitySettings::slotCopyToClipboard);
_issuesWidget = new IssuesWidget(this);
_syncIssueTabId = _tab->addTab(_issuesWidget, Theme::instance()->syncStateIcon(SyncResult::Problem), QString());
slotShowIssueItemCount(0); // to display the label.
connect(_issuesWidget, &IssuesWidget::issueCountUpdated,
this, &ActivitySettings::slotShowIssueItemCount);
connect(_issuesWidget, &IssuesWidget::copyToClipboard,
this, &ActivitySettings::slotCopyToClipboard);
// Add a progress indicator to spin if the acitivity list is updated.
_progressIndicator = new QProgressIndicator(this);
_tab->setCornerWidget(_progressIndicator);
connect(&_notificationCheckTimer, &QTimer::timeout,
this, &ActivitySettings::slotRegularNotificationCheck);
// connect a model signal to stop the animation.
connect(_activityWidget, &ActivityWidget::rowsInserted, _progressIndicator, &QProgressIndicator::stopAnimation);
// Add a progress indicator to spin if the acitivity list is updated.
_progressIndicator = new QProgressIndicator(this);
// We want the protocol be the default
_tab->setCurrentIndex(1);
// connect a model signal to stop the animation
connect(_activityWidget, &ActivityWidget::rowsInserted, _progressIndicator, &QProgressIndicator::stopAnimation);
connect(_activityWidget, &ActivityWidget::rowsInserted, this, &ActivitySettings::slotDisplayActivities);
}
void ActivitySettings::slotDisplayActivities(){
_vbox->removeWidget(_progressIndicator);
}
void ActivitySettings::setNotificationRefreshInterval(std::chrono::milliseconds interval)
@ -539,118 +641,60 @@ void ActivitySettings::setNotificationRefreshInterval(std::chrono::milliseconds
_notificationCheckTimer.start(interval.count());
}
void ActivitySettings::setActivityTabHidden(bool hidden)
{
if (hidden && _activityTabId > -1) {
_tab->removeTab(_activityTabId);
_activityTabId = -1;
_protocolTabId -= 1;
_syncIssueTabId -= 1;
}
if (!hidden && _activityTabId == -1) {
_activityTabId = _tab->insertTab(0, _activityWidget, Theme::instance()->applicationIcon(), tr("Server Activity"));
_protocolTabId += 1;
_syncIssueTabId += 1;
}
}
void ActivitySettings::slotShowIssueItemCount(int cnt)
{
QString cntText = tr("Not Synced");
if (cnt) {
//: %1 is the number of not synced files.
cntText = tr("Not Synced (%1)").arg(cnt);
}
_tab->setTabText(_syncIssueTabId, cntText);
}
void ActivitySettings::slotShowActivityTab()
{
if (_activityTabId != -1) {
_tab->setCurrentIndex(_activityTabId);
}
}
void ActivitySettings::slotShowIssuesTab(const QString &folderAlias)
{
if (_syncIssueTabId == -1)
return;
_tab->setCurrentIndex(_syncIssueTabId);
_issuesWidget->showFolderErrors(folderAlias);
}
void ActivitySettings::slotCopyToClipboard()
{
QString text;
QTextStream ts(&text);
int idx = _tab->currentIndex();
QString message;
if (idx == _activityTabId) {
// the activity widget
_activityWidget->storeActivityList(ts);
message = tr("The server activity list has been copied to the clipboard.");
} else if (idx == _protocolTabId) {
// the protocol widget
_protocolWidget->storeSyncActivity(ts);
message = tr("The sync activity list has been copied to the clipboard.");
} else if (idx == _syncIssueTabId) {
// issues Widget
message = tr("The list of unsynced items has been copied to the clipboard.");
_issuesWidget->storeSyncIssues(ts);
}
_activityWidget->storeActivityList(ts);
message = tr("The server activity and notifications list has been copied to the clipboard.");
QApplication::clipboard()->setText(text);
emit guiLog(tr("Copied to clipboard"), message);
}
void ActivitySettings::slotRemoveAccount(AccountState *ptr)
void ActivitySettings::slotRemoveAccount()
{
_activityWidget->slotRemoveAccount(ptr);
_activityWidget->slotRemoveAccount();
}
void ActivitySettings::slotRefresh(AccountState *ptr)
void ActivitySettings::slotRefresh()
{
// QElapsedTimer isn't actually constructed as invalid.
if (!_timeSinceLastCheck.contains(ptr)) {
_timeSinceLastCheck[ptr].invalidate();
if (!_timeSinceLastCheck.contains(_accountState)) {
_timeSinceLastCheck[_accountState].invalidate();
}
QElapsedTimer &timer = _timeSinceLastCheck[ptr];
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 (ptr && ptr->isConnected()) {
if (_accountState && _accountState->isConnected()) {
if (isVisible() || !timer.isValid()) {
_vbox->insertWidget(0, _progressIndicator);
_vbox->setAlignment(_progressIndicator, Qt::AlignHCenter);
_progressIndicator->startAnimation();
_activityWidget->slotRefreshActivities(ptr);
_activityWidget->slotRefreshActivities();
}
_activityWidget->slotRefreshNotifications(ptr);
_activityWidget->slotRefreshNotifications();
timer.start();
}
}
void ActivitySettings::slotRegularNotificationCheck()
{
AccountManager *am = AccountManager::instance();
foreach (AccountStatePtr a, am->accounts()) {
slotRefresh(a.data());
}
slotRefresh();
}
bool ActivitySettings::event(QEvent *e)
{
if (e->type() == QEvent::Show) {
AccountManager *am = AccountManager::instance();
foreach (AccountStatePtr a, am->accounts()) {
slotRefresh(a.data());
}
slotRefresh();
}
return QWidget::event(e);
}

View file

@ -25,6 +25,7 @@
#include "owncloudgui.h"
#include "account.h"
#include "activitydata.h"
#include "accountmanager.h"
#include "ui_activitywidget.h"
@ -35,8 +36,6 @@ namespace OCC {
class Account;
class AccountStatusPtr;
class ProtocolWidget;
class IssuesWidget;
class JsonApiJob;
class NotificationWidget;
class ActivityListModel;
@ -58,7 +57,7 @@ class ActivityWidget : public QWidget
{
Q_OBJECT
public:
explicit ActivityWidget(QWidget *parent = 0);
explicit ActivityWidget(AccountState *accountState, QWidget *parent = 0);
~ActivityWidget();
QSize sizeHint() const Q_DECL_OVERRIDE { return ownCloudGui::settingsDialogSize(); }
void storeActivityList(QTextStream &ts);
@ -73,34 +72,38 @@ public:
public slots:
void slotOpenFile(QModelIndex indx);
void slotRefreshActivities(AccountState *ptr);
void slotRefreshNotifications(AccountState *ptr);
void slotRemoveAccount(AccountState *ptr);
void slotAccountActivityStatus(AccountState *ast, int statusCode);
void slotRefreshActivities();
void slotRefreshNotifications();
void slotRemoveAccount();
void slotAccountActivityStatus(int statusCode);
void slotRequestCleanupAndBlacklist(const Activity &blacklistActivity);
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);
signals:
void guiLog(const QString &, const QString &);
void copyToClipboard();
void rowsInserted();
void hideActivityTab(bool);
void newNotification();
void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
private slots:
void slotBuildNotificationDisplay(const ActivityList &list);
void slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb);
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(NotificationWidget *widget, int replyCode);
void scheduleWidgetToRemove(NotificationWidget *widget, int milliseconds = 100);
void slotCheckToCleanWidgets();
void slotNotificationRequestFinished(int statusCode);
void slotPrimaryButtonClickedOnListView(const QModelIndex &index);
void slotSecondaryButtonClickedOnListView(const QModelIndex &index);
private:
void showLabels();
QString timeString(QDateTime dt, QLocale::FormatType format) const;
Ui::ActivityWidget *_ui;
QPushButton *_copyBtn;
QSet<QString> _accountsWithoutActivities;
QMap<Activity::Identifier, NotificationWidget *> _widgetForNotifId;
QElapsedTimer _guiLogTimer;
@ -115,7 +118,9 @@ private:
int _notificationRequestsRunning;
ActivityListModel *_model;
QVBoxLayout *_notificationsLayout;
AccountState *_accountState;
const QString _accept;
const QString _remote_share;
};
@ -130,24 +135,20 @@ class ActivitySettings : public QWidget
{
Q_OBJECT
public:
explicit ActivitySettings(QWidget *parent = 0);
explicit ActivitySettings(AccountState *accountState, QWidget *parent = 0);
~ActivitySettings();
QSize sizeHint() const Q_DECL_OVERRIDE { return ownCloudGui::settingsDialogSize(); }
public slots:
void slotRefresh(AccountState *ptr);
void slotRemoveAccount(AccountState *ptr);
void slotRefresh();
void slotRemoveAccount();
void setNotificationRefreshInterval(std::chrono::milliseconds interval);
void slotShowIssuesTab(const QString &folderAlias);
private slots:
void slotCopyToClipboard();
void setActivityTabHidden(bool hidden);
void slotRegularNotificationCheck();
void slotShowIssueItemCount(int cnt);
void slotShowActivityTab();
void slotDisplayActivities();
signals:
void guiLog(const QString &, const QString &);
@ -155,17 +156,13 @@ signals:
private:
bool event(QEvent *e) Q_DECL_OVERRIDE;
QTabWidget *_tab;
int _activityTabId;
int _protocolTabId;
int _syncIssueTabId;
ActivityWidget *_activityWidget;
ProtocolWidget *_protocolWidget;
IssuesWidget *_issuesWidget;
QProgressIndicator *_progressIndicator;
QVBoxLayout *_vbox;
QTimer _notificationCheckTimer;
QHash<AccountState *, QElapsedTimer> _timeSinceLastCheck;
AccountState *_accountState;
};
}
#endif // ActivityWIDGET_H

View file

@ -10,69 +10,75 @@
<height>556</height>
</rect>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>252</red>
<green>252</green>
<blue>252</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>252</red>
<green>252</green>
<blue>252</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="_notifyLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QScrollArea" name="_notifyScroll">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<widget class="QWidget" name="_scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>677</width>
<height>70</height>
</rect>
</property>
</widget>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="_headerLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QListView" name="_activityList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@ -80,9 +86,106 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</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 row="4" column="0">
<item row="1" column="0">
<widget class="QLabel" name="_bottomLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
@ -98,14 +201,32 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QDialogButtonBox" name="_dialogButtonBox"/>
<item row="3" column="0">
<widget class="QPushButton" name="_copyButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>30</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">margin-left:40px;</string>
</property>
<property name="text">
<string>Copy</string>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>_activityList</tabstop>
<tabstop>_dialogButtonBox</tabstop>
</tabstops>
<resources/>
<connections/>

37
src/gui/iconjob.cpp Normal file
View file

@ -0,0 +1,37 @@
/*
* Copyright (C) by Camila Ayres <hello@camila.codes>
*
* 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 "iconjob.h"
namespace OCC {
IconJob::IconJob(const QUrl &url, QObject *parent) :
QObject(parent)
{
connect(&_accessManager, &QNetworkAccessManager::finished,
this, &IconJob::finished);
QNetworkRequest request(url);
_accessManager.get(request);
}
void IconJob::finished(QNetworkReply *reply)
{
if (reply->error() != QNetworkReply::NoError)
return;
reply->deleteLater();
emit jobFinished(reply->readAll());
}
}

47
src/gui/iconjob.h Normal file
View file

@ -0,0 +1,47 @@
/*
* Copyright (C) by Camila Ayres <hello@camila.codes>
*
* 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 ICONJOB_H
#define ICONJOB_H
#include <QObject>
#include <QByteArray>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
namespace OCC {
/**
* @brief Job to fetch a icon
* @ingroup gui
*/
class IconJob : public QObject
{
Q_OBJECT
public:
explicit IconJob(const QUrl &url, QObject *parent = 0);
signals:
void jobFinished(QByteArray iconData);
private slots:
void finished(QNetworkReply *reply);
private:
QNetworkAccessManager _accessManager;
};
}
#endif // ICONJOB_H

View file

@ -1,540 +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 "issueswidget.h"
#include "configfile.h"
#include "syncresult.h"
#include "syncengine.h"
#include "logger.h"
#include "theme.h"
#include "folderman.h"
#include "syncfileitem.h"
#include "folder.h"
#include "openfilemanager.h"
#include "activityitemdelegate.h"
#include "protocolwidget.h"
#include "accountstate.h"
#include "account.h"
#include "accountmanager.h"
#include "common/syncjournalfilerecord.h"
#include "elidedlabel.h"
#include "ui_issueswidget.h"
#include <climits>
namespace OCC {
/**
* If more issues are reported than this they will not show up
* to avoid performance issues around sorting this many issues.
*/
static const int maxIssueCount = 50000;
static QPair<QString, QString> pathsWithIssuesKey(const ProtocolItem::ExtraData &data)
{
return qMakePair(data.folderName, data.path);
}
IssuesWidget::IssuesWidget(QWidget *parent)
: QWidget(parent)
, _ui(new Ui::IssuesWidget)
{
_ui->setupUi(this);
connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
this, &IssuesWidget::slotProgressInfo);
connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
this, &IssuesWidget::slotItemCompleted);
connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
this, &IssuesWidget::addError);
connect(_ui->_treeWidget, &QTreeWidget::itemActivated, this, &IssuesWidget::slotOpenFile);
connect(_ui->copyIssuesButton, &QAbstractButton::clicked, this, &IssuesWidget::copyToClipboard);
_ui->_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
connect(_ui->_treeWidget, &QTreeWidget::customContextMenuRequested, this, &IssuesWidget::slotItemContextMenu);
connect(_ui->showIgnores, &QAbstractButton::toggled, this, &IssuesWidget::slotRefreshIssues);
connect(_ui->showWarnings, &QAbstractButton::toggled, this, &IssuesWidget::slotRefreshIssues);
connect(_ui->filterAccount, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotRefreshIssues);
connect(_ui->filterAccount, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotUpdateFolderFilters);
connect(_ui->filterFolder, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotRefreshIssues);
for (auto account : AccountManager::instance()->accounts()) {
slotAccountAdded(account.data());
}
connect(AccountManager::instance(), &AccountManager::accountAdded,
this, &IssuesWidget::slotAccountAdded);
connect(AccountManager::instance(), &AccountManager::accountRemoved,
this, &IssuesWidget::slotAccountRemoved);
connect(FolderMan::instance(), &FolderMan::folderListChanged,
this, &IssuesWidget::slotUpdateFolderFilters);
// Adjust copyToClipboard() when making changes here!
QStringList header;
header << tr("Time");
header << tr("File");
header << tr("Folder");
header << tr("Issue");
int timestampColumnExtra = 0;
#ifdef Q_OS_WIN
timestampColumnExtra = 20; // font metrics are broken on Windows, see #4721
#endif
_ui->_treeWidget->setHeaderLabels(header);
int timestampColumnWidth =
ActivityItemDelegate::rowHeight() // icon
+ _ui->_treeWidget->fontMetrics().width(ProtocolItem::timeString(QDateTime::currentDateTime()))
+ timestampColumnExtra;
_ui->_treeWidget->setColumnWidth(0, timestampColumnWidth);
_ui->_treeWidget->setColumnWidth(1, 180);
_ui->_treeWidget->setColumnCount(4);
_ui->_treeWidget->setRootIsDecorated(false);
_ui->_treeWidget->setTextElideMode(Qt::ElideMiddle);
_ui->_treeWidget->header()->setObjectName("ActivityErrorListHeader");
#if defined(Q_OS_MAC)
_ui->_treeWidget->setMinimumWidth(400);
#endif
_reenableSorting.setInterval(5000);
connect(&_reenableSorting, &QTimer::timeout, this,
[this]() { _ui->_treeWidget->setSortingEnabled(true); });
_ui->_tooManyIssuesWarning->hide();
connect(this, &IssuesWidget::issueCountUpdated, this,
[this](int count) { _ui->_tooManyIssuesWarning->setVisible(count >= maxIssueCount); });
_ui->_conflictHelp->hide();
_ui->_conflictHelp->setText(
tr("There were conflicts. <a href=\"%1\">Check the documentation on how to resolve them.</a>")
.arg(Theme::instance()->conflictHelpUrl()));
}
IssuesWidget::~IssuesWidget()
{
delete _ui;
}
void IssuesWidget::showEvent(QShowEvent *ev)
{
ConfigFile cfg;
cfg.restoreGeometryHeader(_ui->_treeWidget->header());
// Sorting by section was newly enabled. But if we restore the header
// from a state where sorting was disabled, both of these flags will be
// false and sorting will be impossible!
_ui->_treeWidget->header()->setSectionsClickable(true);
_ui->_treeWidget->header()->setSortIndicatorShown(true);
// Switch back to "first important, then by time" ordering
_ui->_treeWidget->sortByColumn(0, Qt::DescendingOrder);
QWidget::showEvent(ev);
}
void IssuesWidget::hideEvent(QHideEvent *ev)
{
ConfigFile cfg;
cfg.saveGeometryHeader(_ui->_treeWidget->header());
QWidget::hideEvent(ev);
}
static bool persistsUntilLocalDiscovery(QTreeWidgetItem *item)
{
const auto data = ProtocolItem::extraData(item);
return data.status == SyncFileItem::Conflict
|| (data.status == SyncFileItem::FileIgnored && data.direction == SyncFileItem::Up);
}
void IssuesWidget::cleanItems(const std::function<bool(QTreeWidgetItem *)> &shouldDelete)
{
_ui->_treeWidget->setSortingEnabled(false);
// The issue list is a state, clear it and let the next sync fill it
// with ignored files and propagation errors.
int itemCnt = _ui->_treeWidget->topLevelItemCount();
for (int cnt = itemCnt - 1; cnt >= 0; cnt--) {
QTreeWidgetItem *item = _ui->_treeWidget->topLevelItem(cnt);
if (shouldDelete(item)) {
_pathsWithIssues.remove(pathsWithIssuesKey(ProtocolItem::extraData(item)));
delete item;
}
}
_ui->_treeWidget->setSortingEnabled(true);
// update the tabtext
emit(issueCountUpdated(_ui->_treeWidget->topLevelItemCount()));
}
void IssuesWidget::addItem(QTreeWidgetItem *item)
{
if (!item)
return;
int count = _ui->_treeWidget->topLevelItemCount();
if (count >= maxIssueCount)
return;
_ui->_treeWidget->setSortingEnabled(false);
_reenableSorting.start();
// Insert item specific errors behind the others
int insertLoc = 0;
if (!item->text(1).isEmpty()) {
for (int i = 0; i < count; ++i) {
if (_ui->_treeWidget->topLevelItem(i)->text(1).isEmpty()) {
insertLoc = i + 1;
} else {
break;
}
}
}
// Wipe any existing message for the same folder and path
auto newData = ProtocolItem::extraData(item);
if (_pathsWithIssues.contains(pathsWithIssuesKey(newData))) {
for (int i = 0; i < count; ++i) {
auto otherItem = _ui->_treeWidget->topLevelItem(i);
auto otherData = ProtocolItem::extraData(otherItem);
if (otherData.path == newData.path && otherData.folderName == newData.folderName) {
delete otherItem;
break;
}
}
}
_ui->_treeWidget->insertTopLevelItem(insertLoc, item);
_pathsWithIssues.insert(pathsWithIssuesKey(newData));
item->setHidden(!shouldBeVisible(item, currentAccountFilter(), currentFolderFilter()));
emit issueCountUpdated(_ui->_treeWidget->topLevelItemCount());
}
void IssuesWidget::slotOpenFile(QTreeWidgetItem *item, int)
{
QString fileName = item->text(1);
if (Folder *folder = ProtocolItem::folder(item)) {
// folder->path() always comes back with trailing path
QString fullPath = folder->path() + fileName;
if (QFile(fullPath).exists()) {
showInFileManager(fullPath);
}
}
}
void IssuesWidget::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();
cleanItems([&](QTreeWidgetItem *item) {
if (ProtocolItem::extraData(item).folderName != folder)
return false;
if (style == LocalDiscoveryStyle::FilesystemOnly)
return true;
if (!persistsUntilLocalDiscovery(item))
return true;
// Definitely wipe the entry if the file no longer exists
if (!QFileInfo(f->path() + ProtocolItem::extraData(item).path).exists())
return true;
auto path = QFileInfo(ProtocolItem::extraData(item).path).dir().path().toUtf8();
if (path == ".")
path.clear();
return engine.shouldDiscoverLocally(path);
});
}
if (progress.status() == ProgressInfo::Done) {
// We keep track very well of pending conflicts.
// Inform other components about them.
QStringList conflicts;
auto tree = _ui->_treeWidget;
for (int i = 0; i < tree->topLevelItemCount(); ++i) {
auto item = tree->topLevelItem(i);
auto data = ProtocolItem::extraData(item);
if (data.folderName == folder
&& data.status == SyncFileItem::Conflict) {
conflicts.append(data.path);
}
}
emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts);
_ui->_conflictHelp->setHidden(Theme::instance()->conflictHelpUrl().isEmpty() || conflicts.isEmpty());
}
}
void IssuesWidget::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
{
if (!item->showInIssuesTab())
return;
QTreeWidgetItem *line = ProtocolItem::create(folder, *item);
if (!line)
return;
addItem(line);
}
void IssuesWidget::slotRefreshIssues()
{
auto tree = _ui->_treeWidget;
auto filterFolderAlias = currentFolderFilter();
auto filterAccount = currentAccountFilter();
for (int i = 0; i < tree->topLevelItemCount(); ++i) {
auto item = tree->topLevelItem(i);
item->setHidden(!shouldBeVisible(item, filterAccount, filterFolderAlias));
}
_ui->_treeWidget->setColumnHidden(2, !filterFolderAlias.isEmpty());
}
void IssuesWidget::slotAccountAdded(AccountState *account)
{
_ui->filterAccount->addItem(account->account()->displayName(), QVariant::fromValue(account));
updateAccountChoiceVisibility();
}
void IssuesWidget::slotAccountRemoved(AccountState *account)
{
for (int i = _ui->filterAccount->count() - 1; i >= 0; --i) {
if (account == _ui->filterAccount->itemData(i).value<AccountState *>())
_ui->filterAccount->removeItem(i);
}
updateAccountChoiceVisibility();
}
void IssuesWidget::slotItemContextMenu(const QPoint &pos)
{
auto item = _ui->_treeWidget->itemAt(pos);
if (!item)
return;
auto globalPos = _ui->_treeWidget->viewport()->mapToGlobal(pos);
ProtocolItem::openContextMenu(globalPos, item, this);
}
void IssuesWidget::updateAccountChoiceVisibility()
{
bool visible = _ui->filterAccount->count() > 2;
_ui->filterAccount->setVisible(visible);
_ui->accountLabel->setVisible(visible);
slotUpdateFolderFilters();
}
AccountState *IssuesWidget::currentAccountFilter() const
{
return _ui->filterAccount->currentData().value<AccountState *>();
}
QString IssuesWidget::currentFolderFilter() const
{
return _ui->filterFolder->currentData().toString();
}
bool IssuesWidget::shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAccount,
const QString &filterFolderAlias) const
{
bool visible = true;
auto data = ProtocolItem::extraData(item);
auto status = data.status;
visible &= (_ui->showIgnores->isChecked() || status != SyncFileItem::FileIgnored);
visible &= (_ui->showWarnings->isChecked()
|| (status != SyncFileItem::SoftError
&& status != SyncFileItem::Restoration));
const auto &folderalias = data.folderName;
if (filterAccount) {
auto folder = FolderMan::instance()->folder(folderalias);
visible &= folder && folder->accountState() == filterAccount;
}
visible &= (filterFolderAlias.isEmpty() || filterFolderAlias == folderalias);
return visible;
}
void IssuesWidget::slotUpdateFolderFilters()
{
auto account = _ui->filterAccount->currentData().value<AccountState *>();
// If there is no account selector, show folders for the single
// available account
if (_ui->filterAccount->isHidden() && _ui->filterAccount->count() > 1) {
account = _ui->filterAccount->itemData(1).value<AccountState *>();
}
if (!account) {
_ui->filterFolder->setCurrentIndex(0);
}
_ui->filterFolder->setEnabled(account != 0);
for (int i = _ui->filterFolder->count() - 1; i >= 1; --i) {
_ui->filterFolder->removeItem(i);
}
// Find all selectable folders while figuring out if we need a folder
// selector in the first place
bool anyAccountHasMultipleFolders = false;
QSet<AccountState *> accountsWithFolders;
for (auto folder : FolderMan::instance()->map().values()) {
if (accountsWithFolders.contains(folder->accountState()))
anyAccountHasMultipleFolders = true;
accountsWithFolders.insert(folder->accountState());
if (folder->accountState() != account)
continue;
_ui->filterFolder->addItem(folder->shortGuiLocalPath(), folder->alias());
}
// If we don't need the combo box, hide it.
_ui->filterFolder->setVisible(anyAccountHasMultipleFolders);
_ui->folderLabel->setVisible(anyAccountHasMultipleFolders);
// If there's no choice, select the only folder and disable
if (_ui->filterFolder->count() == 2 && anyAccountHasMultipleFolders) {
_ui->filterFolder->setCurrentIndex(1);
_ui->filterFolder->setEnabled(false);
}
}
void IssuesWidget::storeSyncIssues(QTextStream &ts)
{
int topLevelItems = _ui->_treeWidget->topLevelItemCount();
for (int i = 0; i < topLevelItems; i++) {
QTreeWidgetItem *child = _ui->_treeWidget->topLevelItem(i);
if (child->isHidden())
continue;
ts << right
// time stamp
<< qSetFieldWidth(20)
<< child->data(0, Qt::DisplayRole).toString()
// separator
<< qSetFieldWidth(0) << ","
// file name
<< qSetFieldWidth(64)
<< child->data(1, Qt::DisplayRole).toString()
// separator
<< qSetFieldWidth(0) << ","
// folder
<< qSetFieldWidth(30)
<< child->data(2, Qt::DisplayRole).toString()
// separator
<< qSetFieldWidth(0) << ","
// action
<< qSetFieldWidth(15)
<< child->data(3, Qt::DisplayRole).toString()
<< qSetFieldWidth(0)
<< endl;
}
}
void IssuesWidget::showFolderErrors(const QString &folderAlias)
{
auto folder = FolderMan::instance()->folder(folderAlias);
if (!folder)
return;
_ui->filterAccount->setCurrentIndex(
qMax(0, _ui->filterAccount->findData(QVariant::fromValue(folder->accountState()))));
_ui->filterFolder->setCurrentIndex(
qMax(0, _ui->filterFolder->findData(folderAlias)));
_ui->showIgnores->setChecked(false);
_ui->showWarnings->setChecked(false);
}
void IssuesWidget::addError(const QString &folderAlias, const QString &message,
ErrorCategory category)
{
auto folder = FolderMan::instance()->folder(folderAlias);
if (!folder)
return;
QStringList columns;
QDateTime timestamp = QDateTime::currentDateTime();
const QString timeStr = ProtocolItem::timeString(timestamp);
const QString longTimeStr = ProtocolItem::timeString(timestamp, QLocale::LongFormat);
columns << timeStr;
columns << ""; // no "File" entry
columns << folder->shortGuiLocalPath();
columns << message;
QIcon icon = Theme::instance()->syncStateIcon(SyncResult::Error);
QTreeWidgetItem *twitem = new ProtocolItem(columns);
twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
twitem->setIcon(0, icon);
twitem->setToolTip(0, longTimeStr);
twitem->setToolTip(3, message);
ProtocolItem::ExtraData data;
data.timestamp = timestamp;
data.folderName = folderAlias;
data.status = SyncFileItem::NormalError;
ProtocolItem::setExtraData(twitem, data);
addItem(twitem);
addErrorWidget(twitem, message, category);
}
void IssuesWidget::addErrorWidget(QTreeWidgetItem *item, const QString &message, ErrorCategory category)
{
QWidget *widget = 0;
if (category == ErrorCategory::InsufficientRemoteStorage) {
widget = new QWidget;
auto layout = new QHBoxLayout;
widget->setLayout(layout);
auto label = new ElidedLabel(message, widget);
label->setElideMode(Qt::ElideMiddle);
layout->addWidget(label);
auto button = new QPushButton("Retry all uploads", widget);
button->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
auto folderAlias = ProtocolItem::extraData(item).folderName;
connect(button, &QPushButton::clicked,
this, [this, folderAlias]() { retryInsufficentRemoteStorageErrors(folderAlias); });
layout->addWidget(button);
}
if (widget) {
item->setText(3, QString());
}
_ui->_treeWidget->setItemWidget(item, 3, widget);
}
void IssuesWidget::retryInsufficentRemoteStorageErrors(const QString &folderAlias)
{
auto folderman = FolderMan::instance();
auto folder = folderman->folder(folderAlias);
if (!folder)
return;
folder->journalDb()->wipeErrorBlacklistCategory(SyncJournalErrorBlacklistRecord::InsufficientRemoteStorage);
folderman->scheduleFolderNext(folder);
}
}

View file

@ -1,99 +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 ISSUESWIDGET_H
#define ISSUESWIDGET_H
#include <QDialog>
#include <QDateTime>
#include <QLocale>
#include <QTimer>
#include "progressdispatcher.h"
#include "owncloudgui.h"
#include "ui_issueswidget.h"
class QPushButton;
namespace OCC {
class SyncResult;
namespace Ui {
class ProtocolWidget;
}
class Application;
/**
* @brief The ProtocolWidget class
* @ingroup gui
*/
class IssuesWidget : public QWidget
{
Q_OBJECT
public:
explicit IssuesWidget(QWidget *parent = 0);
~IssuesWidget();
QSize sizeHint() const { return ownCloudGui::settingsDialogSize(); }
void storeSyncIssues(QTextStream &ts);
void showFolderErrors(const QString &folderAlias);
public slots:
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 slotOpenFile(QTreeWidgetItem *item, int);
protected:
void showEvent(QShowEvent *);
void hideEvent(QHideEvent *);
signals:
void copyToClipboard();
void issueCountUpdated(int);
private slots:
void slotRefreshIssues();
void slotUpdateFolderFilters();
void slotAccountAdded(AccountState *account);
void slotAccountRemoved(AccountState *account);
void slotItemContextMenu(const QPoint &pos);
private:
void updateAccountChoiceVisibility();
AccountState *currentAccountFilter() const;
QString currentFolderFilter() const;
bool shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAccount,
const QString &filterFolderAlias) const;
void cleanItems(const std::function<bool(QTreeWidgetItem *)> &shouldDelete);
void addItem(QTreeWidgetItem *item);
/// Add the special error widget for the category, if any
void addErrorWidget(QTreeWidgetItem *item, const QString &message, ErrorCategory category);
/// Wipes all insufficient remote storgage blacklist entries
void retryInsufficentRemoteStorageErrors(const QString &folderAlias);
/// Each insert disables sorting, this timer reenables it
QTimer _reenableSorting;
/// Optimization: keep track of all folder/paths pairs that have an associated issue
QSet<QPair<QString, QString>> _pathsWithIssues;
Ui::IssuesWidget *_ui;
};
}
#endif

View file

@ -1,198 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::IssuesWidget</class>
<widget class="QWidget" name="OCC::IssuesWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>580</width>
<height>578</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="_headerLabel">
<property name="text">
<string>List of issues</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QFormLayout" name="accountFolderLayout">
<item row="0" column="0">
<widget class="QLabel" name="accountLabel">
<property name="text">
<string>Account</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="filterAccount">
<item>
<property name="text">
<string>&lt;no filter&gt;</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="folderLabel">
<property name="text">
<string>Folder</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="filterFolder">
<property name="enabled">
<bool>false</bool>
</property>
<item>
<property name="text">
<string>&lt;no filter&gt;</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="1">
<widget class="QCheckBox" name="showWarnings">
<property name="text">
<string>Show warnings</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="showIgnores">
<property name="text">
<string>Show ignored files</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QTreeWidget" name="_treeWidget">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="columnCount">
<number>4</number>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
<column>
<property name="text">
<string notr="true">2</string>
</property>
</column>
<column>
<property name="text">
<string notr="true">3</string>
</property>
</column>
<column>
<property name="text">
<string notr="true">4</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="_tooManyIssuesWarning">
<property name="text">
<string>There were too many issues. Not all will be visible here.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="_conflictHelp">
<property name="text">
<string>There were conflicts. Check the documentation on how to resolve them.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="copyIssuesButton">
<property name="toolTip">
<string>Copy the issues list to the clipboard.</string>
</property>
<property name="text">
<string>Copy</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -47,7 +47,7 @@ void NotificationWidget::setActivity(const Activity &activity)
_ui._subjectLabel->setText(activity._subject);
_ui._messageLabel->setText(activity._message);
_ui._notifIcon->setPixmap(QPixmap(":/client/resources/bell.png"));
_ui._notifIcon->setPixmap(QPixmap(":/client/resources/bell.svg"));
_ui._notifIcon->setMinimumWidth(22);
_ui._notifIcon->setMinimumHeight(22);
_ui._notifIcon->show();

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>725</width>
<height>125</height>
<height>129</height>
</rect>
</property>
<property name="sizePolicy">
@ -75,7 +75,7 @@
<string/>
</property>
<property name="pixmap">
<pixmap>../../../../resources/bell.png</pixmap>
<pixmap>../../resources/bell.svg</pixmap>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>

View file

@ -1040,7 +1040,7 @@ void ownCloudGui::slotShowSettings()
void ownCloudGui::slotShowSyncProtocol()
{
slotShowSettings();
_settingsDialog->showActivityPage();
//_settingsDialog->showActivityPage();
}

View file

@ -1,330 +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 "protocolwidget.h"
#include "configfile.h"
#include "syncresult.h"
#include "logger.h"
#include "theme.h"
#include "folderman.h"
#include "syncfileitem.h"
#include "folder.h"
#include "openfilemanager.h"
#include "activityitemdelegate.h"
#include "guiutility.h"
#include "accountstate.h"
#include "ui_protocolwidget.h"
#include <climits>
Q_DECLARE_METATYPE(OCC::ProtocolItem::ExtraData)
namespace OCC {
QString ProtocolItem::timeString(QDateTime dt, QLocale::FormatType format)
{
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);
}
ProtocolItem::ExtraData ProtocolItem::extraData(const QTreeWidgetItem *item)
{
return item->data(0, Qt::UserRole).value<ExtraData>();
}
void ProtocolItem::setExtraData(QTreeWidgetItem *item, const ExtraData &data)
{
item->setData(0, Qt::UserRole, QVariant::fromValue(data));
}
ProtocolItem *ProtocolItem::create(const QString &folder, const SyncFileItem &item)
{
auto f = FolderMan::instance()->folder(folder);
if (!f) {
return 0;
}
QStringList columns;
QDateTime timestamp = QDateTime::currentDateTime();
const QString timeStr = timeString(timestamp);
const QString longTimeStr = timeString(timestamp, QLocale::LongFormat);
columns << timeStr;
columns << Utility::fileNameForGuiUse(item._originalFile);
columns << f->shortGuiLocalPath();
// If the error string is set, it's prefered because it is a useful user message.
QString message = item._errorString;
if (message.isEmpty()) {
message = Progress::asResultString(item);
}
columns << message;
QIcon icon;
if (item._status == SyncFileItem::NormalError
|| item._status == SyncFileItem::FatalError
|| item._status == SyncFileItem::DetailError
|| item._status == SyncFileItem::BlacklistedError) {
icon = Theme::instance()->syncStateIcon(SyncResult::Error);
} else if (Progress::isWarningKind(item._status)) {
icon = Theme::instance()->syncStateIcon(SyncResult::Problem);
}
if (ProgressInfo::isSizeDependent(item)) {
columns << Utility::octetsToString(item._size);
}
ProtocolItem *twitem = new ProtocolItem(columns);
// Warning: The data and tooltips on the columns define an implicit
// interface and can only be changed with care.
twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
twitem->setIcon(0, icon);
twitem->setToolTip(0, longTimeStr);
twitem->setToolTip(1, item._file);
twitem->setToolTip(3, message);
ProtocolItem::ExtraData data;
data.timestamp = timestamp;
data.path = item._file;
data.folderName = folder;
data.status = item._status;
data.size = item._size;
data.direction = item._direction;
ProtocolItem::setExtraData(twitem, data);
return twitem;
}
SyncJournalFileRecord ProtocolItem::syncJournalRecord(QTreeWidgetItem *item)
{
SyncJournalFileRecord rec;
auto f = folder(item);
if (!f)
return rec;
f->journalDb()->getFileRecord(extraData(item).path, &rec);
return rec;
}
Folder *ProtocolItem::folder(QTreeWidgetItem *item)
{
return FolderMan::instance()->folder(extraData(item).folderName);
}
void ProtocolItem::openContextMenu(QPoint globalPos, QTreeWidgetItem *item, QWidget *parent)
{
auto f = folder(item);
if (!f)
return;
AccountPtr account = f->accountState()->account();
auto rec = syncJournalRecord(item);
// rec might not be valid
auto menu = new QMenu(parent);
if (rec.isValid()) {
// "Open in Browser" action
auto openInBrowser = menu->addAction(ProtocolWidget::tr("Open in browser"));
QObject::connect(openInBrowser, &QAction::triggered, parent, [parent, account, rec]() {
fetchPrivateLinkUrl(account, rec._path, rec.numericFileId(), parent,
[parent](const QString &url) {
Utility::openBrowser(url, parent);
});
});
}
// More actions will be conditionally added to the context menu here later
if (menu->actions().isEmpty()) {
delete menu;
return;
}
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(globalPos);
}
bool ProtocolItem::operator<(const QTreeWidgetItem &other) const
{
int column = treeWidget()->sortColumn();
if (column == 0) {
// Items with empty "File" column are larger than others,
// otherwise sort by time (this uses lexicographic ordering)
return std::forward_as_tuple(text(1).isEmpty(), extraData(this).timestamp)
< std::forward_as_tuple(other.text(1).isEmpty(), extraData(&other).timestamp);
} else if (column == 4) {
return extraData(this).size < extraData(&other).size;
}
return QTreeWidgetItem::operator<(other);
}
ProtocolWidget::ProtocolWidget(QWidget *parent)
: QWidget(parent)
, _ui(new Ui::ProtocolWidget)
{
_ui->setupUi(this);
connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
this, &ProtocolWidget::slotItemCompleted);
connect(_ui->_treeWidget, &QTreeWidget::itemActivated, this, &ProtocolWidget::slotOpenFile);
_ui->_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
connect(_ui->_treeWidget, &QTreeWidget::customContextMenuRequested, this, &ProtocolWidget::slotItemContextMenu);
// Adjust copyToClipboard() when making changes here!
QStringList header;
header << tr("Time");
header << tr("File");
header << tr("Folder");
header << tr("Action");
header << tr("Size");
int timestampColumnExtra = 0;
#ifdef Q_OS_WIN
timestampColumnExtra = 20; // font metrics are broken on Windows, see #4721
#endif
_ui->_treeWidget->setHeaderLabels(header);
int timestampColumnWidth =
_ui->_treeWidget->fontMetrics().width(ProtocolItem::timeString(QDateTime::currentDateTime()))
+ timestampColumnExtra;
_ui->_treeWidget->setColumnWidth(0, timestampColumnWidth);
_ui->_treeWidget->setColumnWidth(1, 180);
_ui->_treeWidget->setColumnCount(5);
_ui->_treeWidget->setRootIsDecorated(false);
_ui->_treeWidget->setTextElideMode(Qt::ElideMiddle);
_ui->_treeWidget->header()->setObjectName("ActivityListHeader");
#if defined(Q_OS_MAC)
_ui->_treeWidget->setMinimumWidth(400);
#endif
_ui->_headerLabel->setText(tr("Local sync protocol"));
QPushButton *copyBtn = _ui->_dialogButtonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole);
copyBtn->setToolTip(tr("Copy the activity list to the clipboard."));
copyBtn->setEnabled(true);
connect(copyBtn, &QAbstractButton::clicked, this, &ProtocolWidget::copyToClipboard);
}
ProtocolWidget::~ProtocolWidget()
{
delete _ui;
}
void ProtocolWidget::showEvent(QShowEvent *ev)
{
ConfigFile cfg;
cfg.restoreGeometryHeader(_ui->_treeWidget->header());
// Sorting by section was newly enabled. But if we restore the header
// from a state where sorting was disabled, both of these flags will be
// false and sorting will be impossible!
_ui->_treeWidget->header()->setSectionsClickable(true);
_ui->_treeWidget->header()->setSortIndicatorShown(true);
// Switch back to "by time" ordering
_ui->_treeWidget->sortByColumn(0, Qt::DescendingOrder);
QWidget::showEvent(ev);
}
void ProtocolWidget::hideEvent(QHideEvent *ev)
{
ConfigFile cfg;
cfg.saveGeometryHeader(_ui->_treeWidget->header());
QWidget::hideEvent(ev);
}
void ProtocolWidget::slotItemContextMenu(const QPoint &pos)
{
auto item = _ui->_treeWidget->itemAt(pos);
if (!item)
return;
auto globalPos = _ui->_treeWidget->viewport()->mapToGlobal(pos);
ProtocolItem::openContextMenu(globalPos, item, this);
}
void ProtocolWidget::slotOpenFile(QTreeWidgetItem *item, int)
{
QString fileName = item->text(1);
if (Folder *folder = ProtocolItem::folder(item)) {
// folder->path() always comes back with trailing path
QString fullPath = folder->path() + fileName;
if (QFile(fullPath).exists()) {
showInFileManager(fullPath);
}
}
}
void ProtocolWidget::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
{
if (!item->showInProtocolTab())
return;
QTreeWidgetItem *line = ProtocolItem::create(folder, *item);
if (line) {
// Limit the number of items
int itemCnt = _ui->_treeWidget->topLevelItemCount();
while (itemCnt > 2000) {
delete _ui->_treeWidget->takeTopLevelItem(itemCnt - 1);
itemCnt--;
}
_ui->_treeWidget->insertTopLevelItem(0, line);
}
}
void ProtocolWidget::storeSyncActivity(QTextStream &ts)
{
int topLevelItems = _ui->_treeWidget->topLevelItemCount();
for (int i = 0; i < topLevelItems; i++) {
QTreeWidgetItem *child = _ui->_treeWidget->topLevelItem(i);
ts << right
// time stamp
<< qSetFieldWidth(20)
<< child->data(0, Qt::DisplayRole).toString()
// separator
<< qSetFieldWidth(0) << ","
// file name
<< qSetFieldWidth(64)
<< child->data(1, Qt::DisplayRole).toString()
// separator
<< qSetFieldWidth(0) << ","
// folder
<< qSetFieldWidth(30)
<< child->data(2, Qt::DisplayRole).toString()
// separator
<< qSetFieldWidth(0) << ","
// action
<< qSetFieldWidth(15)
<< child->data(3, Qt::DisplayRole).toString()
// separator
<< qSetFieldWidth(0) << ","
// size
<< qSetFieldWidth(10)
<< child->data(4, Qt::DisplayRole).toString()
<< qSetFieldWidth(0)
<< endl;
}
}
}

View file

@ -1,112 +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 PROTOCOLWIDGET_H
#define PROTOCOLWIDGET_H
#include <QDialog>
#include <QDateTime>
#include <QLocale>
#include "progressdispatcher.h"
#include "owncloudgui.h"
#include "ui_protocolwidget.h"
class QPushButton;
namespace OCC {
class SyncResult;
namespace Ui {
class ProtocolWidget;
}
class Application;
/**
* The items used in the protocol and issue QTreeWidget
*
* Special sorting: It allows items for global entries to be moved to the top if the
* sorting section is the "Time" column.
*/
class ProtocolItem : public QTreeWidgetItem
{
public:
using QTreeWidgetItem::QTreeWidgetItem;
// Shared with IssueWidget
static ProtocolItem *create(const QString &folder, const SyncFileItem &item);
static QString timeString(QDateTime dt, QLocale::FormatType format = QLocale::NarrowFormat);
struct ExtraData
{
ExtraData()
: status(SyncFileItem::NoStatus)
, direction(SyncFileItem::None)
{
}
QString path;
QString folderName;
QDateTime timestamp;
quint64 size = 0;
SyncFileItem::Status status BITFIELD(4);
SyncFileItem::Direction direction BITFIELD(3);
};
static ExtraData extraData(const QTreeWidgetItem *item);
static void setExtraData(QTreeWidgetItem *item, const ExtraData &data);
static SyncJournalFileRecord syncJournalRecord(QTreeWidgetItem *item);
static Folder *folder(QTreeWidgetItem *item);
static void openContextMenu(QPoint globalPos, QTreeWidgetItem *item, QWidget *parent);
private:
bool operator<(const QTreeWidgetItem &other) const override;
};
/**
* @brief The ProtocolWidget class
* @ingroup gui
*/
class ProtocolWidget : public QWidget
{
Q_OBJECT
public:
explicit ProtocolWidget(QWidget *parent = 0);
~ProtocolWidget();
QSize sizeHint() const { return ownCloudGui::settingsDialogSize(); }
void storeSyncActivity(QTextStream &ts);
public slots:
void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
void slotOpenFile(QTreeWidgetItem *item, int);
protected:
void showEvent(QShowEvent *);
void hideEvent(QHideEvent *);
private slots:
void slotItemContextMenu(const QPoint &pos);
signals:
void copyToClipboard();
private:
Ui::ProtocolWidget *_ui;
};
}
#endif // PROTOCOLWIDGET_H

View file

@ -1,73 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::ProtocolWidget</class>
<widget class="QWidget" name="OCC::ProtocolWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>612</width>
<height>515</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="_headerLabel">
<property name="text">
<string>TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QTreeWidget" name="_treeWidget">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="columnCount">
<number>4</number>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
<column>
<property name="text">
<string notr="true">2</string>
</property>
</column>
<column>
<property name="text">
<string notr="true">3</string>
</property>
</column>
<column>
<property name="text">
<string notr="true">4</string>
</property>
</column>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="_dialogButtonBox"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -17,6 +17,8 @@
#include "capabilities.h"
#include "networkjobs.h"
#include "iconjob.h"
#include <QJsonDocument>
#include <QJsonObject>
@ -28,37 +30,41 @@ 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;
ServerNotificationHandler::ServerNotificationHandler(QObject *parent)
ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState, QObject *parent)
: QObject(parent)
, _accountState(accountState)
{
}
void ServerNotificationHandler::slotFetchNotifications(AccountState *ptr)
void ServerNotificationHandler::slotFetchNotifications()
{
// check connectivity and credentials
if (!(ptr && ptr->isConnected() && ptr->account() && ptr->account()->credentials() && ptr->account()->credentials()->ready())) {
if (!(_accountState && _accountState->isConnected() &&
_accountState->account() && _accountState->account()->credentials() &&
_accountState->account()->credentials()->ready())) {
deleteLater();
return;
}
// check if the account has notifications enabled. If the capabilities are
// not yet valid, its assumed that notifications are available.
if (ptr->account()->capabilities().isValid()) {
if (!ptr->account()->capabilities().notificationsAvailable()) {
qCInfo(lcServerNotification) << "Account" << ptr->account()->displayName() << "does not have notifications enabled.";
if (_accountState->account()->capabilities().isValid()) {
if (!_accountState->account()->capabilities().notificationsAvailable()) {
qCInfo(lcServerNotification) << "Account" << _accountState->account()->displayName() << "does not have notifications enabled.";
deleteLater();
return;
}
}
// if the previous notification job has finished, start next.
_notificationJob = new JsonApiJob(ptr->account(), notificationsPath, this);
_notificationJob = new JsonApiJob(_accountState->account(), notificationsPath, this);
QObject::connect(_notificationJob.data(), &JsonApiJob::jsonReceived,
this, &ServerNotificationHandler::slotNotificationsReceived);
QObject::connect(_notificationJob.data(), &JsonApiJob::etagResponseHeaderReceived,
this, &ServerNotificationHandler::slotEtagResponseHeaderReceived);
_notificationJob->setProperty(propertyAccountStateC, QVariant::fromValue<AccountState *>(ptr));
_notificationJob->addRawHeader("If-None-Match", ptr->notificationsEtagResponseHeader());
_notificationJob->setProperty(propertyAccountStateC, QVariant::fromValue<AccountState *>(_accountState));
_notificationJob->addRawHeader("If-None-Match", _accountState->notificationsEtagResponseHeader());
_notificationJob->start();
}
@ -70,6 +76,12 @@ void ServerNotificationHandler::slotEtagResponseHeaderReceived(const QByteArray
}
}
void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData){
QPixmap pixmap;
pixmap.loadFromData(iconData);
iconCache.insert(sender()->property("activityId").toInt(), QIcon(pixmap));
}
void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &json, int statusCode)
{
if (statusCode != successStatusCode && statusCode != notModifiedStatusCode) {
@ -96,18 +108,28 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
a._type = Activity::NotificationType;
a._accName = ai->account()->displayName();
a._id = json.value("notification_id").toInt();
//need to know, specially for remote_share
a._objectType = json.value("object_type").toString();
a._status = 0;
a._subject = json.value("subject").toString();
a._message = json.value("message").toString();
QString s = json.value("link").toString();
if (!s.isEmpty()) {
QUrl link(s);
if(!json.value("icon").toString().isEmpty()){
IconJob *iconJob = new IconJob(QUrl(json.value("icon").toString()));
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()){
link.setScheme(ai->account()->url().scheme());
link.setHost(ai->account()->url().host());
}
a._link = link;
}
a._link = link;
a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate);
auto actions = json.value("actions").toArray();

View file

@ -27,20 +27,23 @@ class ServerNotificationHandler : public QObject
{
Q_OBJECT
public:
explicit ServerNotificationHandler(QObject *parent = 0);
explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = 0);
static QMap<int, QIcon> iconCache;
signals:
void newNotificationList(ActivityList);
public slots:
void slotFetchNotifications(AccountState *ptr);
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;
};
}

View file

@ -25,7 +25,6 @@
#include "owncloudgui.h"
#include "activitywidget.h"
#include "accountmanager.h"
#include "protocolwidget.h"
#include <QLabel>
#include <QStandardItemModel>
@ -44,8 +43,8 @@
namespace {
const char TOOLBAR_CSS[] =
"QToolBar { background: %1; margin: 0; padding: 0; border: none; border-bottom: 1px solid %2; spacing: 0; } "
"QToolBar QToolButton { background: %1; border: none; border-bottom: 1px solid %2; margin: 0; padding: 5px; } "
"QToolBar { background: %1; margin: 0; padding: 0; border: none; border-bottom: 0 solid %2; spacing: 0; } "
"QToolBar QToolButton { background: %1; border: none; border-bottom: 0 solid %2; margin: 0; padding: 5px; } "
"QToolBar QToolBarExtension { padding:0; } "
"QToolBar QToolButton:checked { background: %3; color: %4; }";
@ -84,19 +83,27 @@ SettingsDialog::SettingsDialog(ownCloudGui *gui, QWidget *parent)
setObjectName("Settings"); // required as group for saveGeometry call
setWindowTitle(Theme::instance()->appNameGUI());
connect(AccountManager::instance(), &AccountManager::accountAdded,
this, &SettingsDialog::accountAdded);
connect(AccountManager::instance(), &AccountManager::accountRemoved,
this, &SettingsDialog::accountRemoved);
_actionGroup = new QActionGroup(this);
_actionGroup->setExclusive(true);
connect(_actionGroup, &QActionGroup::triggered, this, &SettingsDialog::slotSwitchPage);
// 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
_activityAction = createColorAwareAction(QLatin1String(":/client/resources/activity.png"), tr("Activity"));
_actionGroup->addAction(_activityAction);
_toolBar->addAction(_activityAction);
_activitySettings = new ActivitySettings;
_ui->stack->addWidget(_activitySettings);
connect(_activitySettings, &ActivitySettings::guiLog, _gui,
&ownCloudGui::slotShowOptionalTrayMessage);
_activitySettings->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
foreach(auto ai, AccountManager::instance()->accounts()) {
accountAdded(ai.data());
}
_actionBefore = new QAction;
_toolBar->addAction(_actionBefore);
// Adds space between users + activities and general + network actions
QWidget* spacer = new QWidget();
spacer->setMinimumWidth(10);
spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
_toolBar->addWidget(spacer);
QAction *generalAction = createColorAwareAction(QLatin1String(":/client/resources/settings.png"), tr("General"));
_actionGroup->addAction(generalAction);
@ -110,20 +117,9 @@ SettingsDialog::SettingsDialog(ownCloudGui *gui, QWidget *parent)
NetworkSettings *networkSettings = new NetworkSettings;
_ui->stack->addWidget(networkSettings);
_actionGroupWidgets.insert(_activityAction, _activitySettings);
_actionGroupWidgets.insert(generalAction, generalSettings);
_actionGroupWidgets.insert(networkAction, networkSettings);
connect(_actionGroup, &QActionGroup::triggered, this, &SettingsDialog::slotSwitchPage);
connect(AccountManager::instance(), &AccountManager::accountAdded,
this, &SettingsDialog::accountAdded);
connect(AccountManager::instance(), &AccountManager::accountRemoved,
this, &SettingsDialog::accountRemoved);
foreach (auto ai, AccountManager::instance()->accounts()) {
accountAdded(ai.data());
}
QTimer::singleShot(1, this, &SettingsDialog::showFirstPage);
QPushButton *closeButton = _ui->buttonBox->button(QDialogButtonBox::Close);
@ -189,25 +185,43 @@ void SettingsDialog::showFirstPage()
void SettingsDialog::showActivityPage()
{
if (_activityAction) {
_activityAction->trigger();
if (auto account = qvariant_cast<AccountState*>(sender()->property("account"))) {
_activitySettings[account]->show();
_ui->stack->setCurrentWidget(_activitySettings[account]);
}
}
void SettingsDialog::showIssuesList(const QString &folderAlias)
{
if (!_activityAction)
return;
_activityAction->trigger();
_activitySettings->slotShowIssuesTab(folderAlias);
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)
{
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();
@ -223,7 +237,8 @@ void SettingsDialog::accountAdded(AccountState *s)
accountAction->setToolTip(s->account()->displayName());
accountAction->setIconText(SettingsDialogCommon::shortDisplayNameForSettings(s->account().data(), height * buttonSizeRatio));
}
_toolBar->insertAction(_toolBar->actions().at(0), accountAction);
_toolBar->insertAction(_actionBefore, accountAction);
auto accountSettings = new AccountSettings(s, this);
_ui->stack->insertWidget(0, accountSettings);
_actionGroup->addAction(accountAction);
@ -233,13 +248,13 @@ 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);
activityAdded(s);
slotRefreshActivity(s);
}
@ -295,7 +310,19 @@ void SettingsDialog::accountRemoved(AccountState *s)
if (_actionForAccount.contains(s->account().data())) {
_actionForAccount.remove(s->account().data());
}
_activitySettings->slotRemoveAccount(s);
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
@ -317,9 +344,8 @@ void SettingsDialog::customizeStyle()
QIcon icon = createColorAwareIcon(a->property("iconPath").toString());
a->setIcon(icon);
QToolButton *btn = qobject_cast<QToolButton *>(_toolBar->widgetForAction(a));
if (btn) {
if (btn)
btn->setIcon(icon);
}
}
}
@ -359,8 +385,6 @@ public:
btn->setDefaultAction(this);
btn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
// btn->setMinimumWidth(qMax<int>(parent->sizeHint().height() * buttonSizeRatio,
// btn->sizeHint().width()));
return btn;
}
};
@ -389,9 +413,8 @@ void SettingsDialog::slotRefreshActivityAccountStateSender()
void SettingsDialog::slotRefreshActivity(AccountState *accountState)
{
if (accountState) {
_activitySettings->slotRefresh(accountState);
}
if (accountState->isConnected())
_activitySettings[accountState]->slotRefresh();
}
} // namespace OCC

View file

@ -56,7 +56,7 @@ public:
public slots:
void showFirstPage();
void showActivityPage();
void showIssuesList(const QString &folderAlias);
// void showIssuesList(const QString &folderAlias);
void slotSwitchPage(QAction *action);
void slotRefreshActivity(AccountState *accountState);
void slotRefreshActivityAccountStateSender();
@ -74,6 +74,7 @@ private slots:
private:
void customizeStyle();
void activityAdded(AccountState *);
QIcon createColorAwareIcon(const QString &name);
QAction *createColorAwareAction(const QString &iconName, const QString &fileName);
@ -82,6 +83,7 @@ private:
Ui::SettingsDialog *const _ui;
QActionGroup *_actionGroup;
QAction *_actionBefore;
// Maps the actions from the action group to the corresponding widgets
QHash<QAction *, QWidget *> _actionGroupWidgets;
@ -90,10 +92,8 @@ private:
QHash<Account *, QAction *> _actionForAccount;
QToolBar *_toolBar;
QMap<AccountState *, ActivitySettings *> _activitySettings;
ActivitySettings *_activitySettings;
QAction *_activityAction;
ownCloudGui *_gui;
};
}

View file

@ -52,7 +52,75 @@
</widget>
</item>
<item row="0" column="0">
<widget class="QStackedWidget" name="stack"/>
<widget class="QStackedWidget" name="stack">
<property name="palette">
<palette>
<active>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>252</red>
<green>252</green>
<blue>252</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>252</red>
<green>252</green>
<blue>252</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>

View file

@ -26,7 +26,6 @@
#include "configfile.h"
#include "progressdispatcher.h"
#include "owncloudgui.h"
#include "protocolwidget.h"
#include "activitywidget.h"
#include "accountmanager.h"
@ -72,7 +71,6 @@ SettingsDialogMac::SettingsDialogMac(ownCloudGui *gui, QWidget *parent)
// dialog from minimize is broken in MacPreferencesWindow
setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint | Qt::WindowMaximizeButtonHint);
// Emulate dialog behavior: Escape means close
QAction *closeDialogAction = new QAction(this);
closeDialogAction->setShortcut(QKeySequence(Qt::Key_Escape));
@ -93,16 +91,12 @@ SettingsDialogMac::SettingsDialogMac(ownCloudGui *gui, QWidget *parent)
setWindowTitle(tr("%1").arg(Theme::instance()->appNameGUI()));
QIcon activityIcon(QLatin1String(":/client/resources/activity.png"));
_activitySettings = new ActivitySettings;
addPreferencesPanel(activityIcon, tr("Activity"), _activitySettings);
connect(_activitySettings, SIGNAL(guiLog(QString, QString)), _gui,
SLOT(slotShowOptionalTrayMessage(QString, QString)));
connect(AccountManager::instance(), &AccountManager::accountAdded,
this, &SettingsDialogMac::accountAdded);
connect(AccountManager::instance(), &AccountManager::accountRemoved,
this, &SettingsDialogMac::accountRemoved);
_actionsIdx = -1;
foreach (auto ai, AccountManager::instance()->accounts()) {
accountAdded(ai.data());
}
@ -122,7 +116,6 @@ SettingsDialogMac::SettingsDialogMac(ownCloudGui *gui, QWidget *parent)
ConfigFile cfg;
cfg.restoreGeometry(this);
_activitySettings->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
}
void SettingsDialogMac::closeEvent(QCloseEvent *event)
@ -138,26 +131,17 @@ void SettingsDialogMac::showActivityPage()
setCurrentPanelIndex(preferencePanelCount() - 1 - 2);
}
void SettingsDialogMac::showIssuesList(const QString &folderAlias)
{
// Count backwards (0-based) from the last panel (multiple accounts can be on the left)
setCurrentPanelIndex(preferencePanelCount() - 1 - 2);
_activitySettings->slotShowIssuesTab(folderAlias);
}
void SettingsDialogMac::accountAdded(AccountState *s)
{
QIcon accountIcon = MacStandardIcon::icon(MacStandardIcon::UserAccounts);
auto accountSettings = new AccountSettings(s, this);
QString displayName = Theme::instance()->multiAccount() ? SettingsDialogCommon::shortDisplayNameForSettings(s->account().data(), 0) : tr("Account");
insertPreferencesPanel(0, accountIcon, displayName, accountSettings);
// this adds the panel - nothing to add here just to fix the order
insertPreferencesPanel(++_actionsIdx, accountIcon, displayName, accountSettings);
connect(accountSettings, &AccountSettings::folderChanged, _gui, &ownCloudGui::slotFoldersChanged);
connect(accountSettings, &AccountSettings::openFolderAlias, _gui, &ownCloudGui::slotFolderOpenAction);
connect(accountSettings, &AccountSettings::showIssuesList, this, &SettingsDialogMac::showIssuesList);
connect(s->account().data(), &Account::accountChangedAvatar, this, &SettingsDialogMac::slotAccountAvatarChanged);
connect(s->account().data(), &Account::accountChangedDisplayName, this, &SettingsDialogMac::slotAccountDisplayNameChanged);
@ -165,6 +149,23 @@ void SettingsDialogMac::accountAdded(AccountState *s)
// Refresh immediatly when getting online
connect(s, &AccountState::isConnectedChanged, this, &SettingsDialogMac::slotRefreshActivityAccountStateSender);
// Add activity panel
QIcon activityIcon(QLatin1String(":/client/resources/activity.png"));
_activitySettings[s] = new ActivitySettings(s, this);
insertPreferencesPanel(++_actionsIdx, activityIcon, tr("Activity"), _activitySettings[s]);
connect(_activitySettings[s], SIGNAL(guiLog(QString, QString)), _gui,
SLOT(slotShowOptionalTrayMessage(QString, QString)));
// if this is not the first account, add separator 2 positions before int the toolbar
if(AccountManager::instance()->accounts().first().data() != s &&
AccountManager::instance()->accounts().size() >= 1){
_separators[s] = insertSeparator(_actionsIdx - 1);
++_actionsIdx; //we have one more item in the toolbar
}
ConfigFile cfg;
_activitySettings[s]->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
slotRefreshActivity(s);
}
@ -174,10 +175,18 @@ void SettingsDialogMac::accountRemoved(AccountState *s)
foreach (auto p, list) {
if (p->accountsState() == s) {
removePreferencesPanel(p);
// remove settings panel
if(_activitySettings.contains(s))
removePreferencesPanel(_activitySettings[s]);
// remove separator if there is any
if(_separators.contains(s)){
removeSeparator(_separators[s]);
_separators.remove(s);
}
}
}
_activitySettings->slotRemoveAccount(s);
}
void SettingsDialogMac::slotRefreshActivityAccountStateSender()
@ -188,7 +197,7 @@ void SettingsDialogMac::slotRefreshActivityAccountStateSender()
void SettingsDialogMac::slotRefreshActivity(AccountState *accountState)
{
if (accountState) {
_activitySettings->slotRefresh(accountState);
_activitySettings[accountState]->slotRefresh();
}
}
@ -226,3 +235,4 @@ void SettingsDialogMac::slotAccountDisplayNameChanged()
}
}

View file

@ -26,7 +26,6 @@ class QListWidgetItem;
namespace OCC {
class AccountSettings;
class ProtocolWidget;
class Application;
class FolderMan;
class ownCloudGui;
@ -47,7 +46,6 @@ public:
public slots:
void showActivityPage();
void showIssuesList(const QString &folderAlias);
void slotRefreshActivity(AccountState *accountState);
void slotRefreshActivityAccountStateSender();
@ -60,8 +58,11 @@ private slots:
private:
void closeEvent(QCloseEvent *event);
ProtocolWidget *_protocolWidget;
ActivitySettings *_activitySettings;
QAction *_actionBefore;
int _actionsIdx;
QMap<AccountState *, QAction *> _separators;
QMap<AccountState *, ActivitySettings *> _activitySettings;
ownCloudGui *_gui;
int _protocolIdx;
@ -69,3 +70,4 @@ private:
}
#endif // SETTINGSDIALOGMAC_H
;

View file

@ -27,7 +27,7 @@
namespace OCC {
SyncLogDialog::SyncLogDialog(QWidget *parent, ProtocolWidget *protoWidget)
SyncLogDialog::SyncLogDialog(QWidget *parent)
: QDialog(parent)
, _ui(new Ui::SyncLogDialog)
{
@ -35,10 +35,6 @@ SyncLogDialog::SyncLogDialog(QWidget *parent, ProtocolWidget *protoWidget)
_ui->setupUi(this);
if (protoWidget) {
_ui->logWidgetLayout->addWidget(protoWidget);
}
QPushButton *closeButton = _ui->buttonBox->button(QDialogButtonBox::Close);
if (closeButton) {
connect(closeButton, &QAbstractButton::clicked, this, &QWidget::close);

View file

@ -16,8 +16,6 @@
#ifndef SyncLogDialog_H
#define SyncLogDialog_H
#include "protocolwidget.h"
#include <QDialog>
namespace OCC {
@ -37,7 +35,7 @@ class SyncLogDialog : public QDialog
Q_OBJECT
public:
explicit SyncLogDialog(QWidget *parent = 0, ProtocolWidget *protoWidget = 0);
explicit SyncLogDialog(QWidget *parent = 0);
~SyncLogDialog();
private slots: