diff --git a/resources.qrc b/resources.qrc
index 437c9567a..f2f122987 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -5,5 +5,6 @@
src/gui/tray/HeaderButton.qml
theme/Style/Style.qml
theme/Style/qmldir
+ src/gui/tray/ActivityActionButton.qml
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 5a49920e0..b6a666924 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -43,6 +43,7 @@ set(client_UI_SRCS
addcertificatedialog.ui
proxyauthdialog.ui
mnemonicdialog.ui
+ tray/ActivityActionButton.qml
tray/Window.qml
tray/UserLine.qml
wizard/flow2authwidget.ui
diff --git a/src/gui/tray/ActivityActionButton.qml b/src/gui/tray/ActivityActionButton.qml
new file mode 100644
index 000000000..6bb127e71
--- /dev/null
+++ b/src/gui/tray/ActivityActionButton.qml
@@ -0,0 +1,106 @@
+import QtQuick 2.5
+import QtQuick.Controls 2.3
+import Style 1.0
+
+Item {
+ id: root
+ readonly property bool labelVisible: label.visible
+ readonly property bool iconVisible: icon.visible
+
+ // label value
+ property string text: ""
+
+ // icon value
+ property string imageSource: ""
+
+ // Tooltip value
+ property string tooltipText: text
+
+ // text color
+ property color textColor: Style.ncTextColor
+ property color textColorHovered: Style.lightHover
+
+ // text background color
+ property color textBgColor: "transparent"
+ property color textBgColorHovered: Style.lightHover
+
+ // icon background color
+ property color iconBgColor: "transparent"
+ property color iconBgColorHovered: Style.lightHover
+
+ // text border color
+ property color textBorderColor: "transparent"
+
+ property alias hovered: mouseArea.containsMouse
+
+ signal clicked()
+
+ Accessible.role: Accessible.Button
+ Accessible.name: text !== "" ? text : (tooltipText !== "" ? tooltipText : qsTr("Activity action button"))
+ Accessible.onPressAction: clicked()
+
+ // background with border around the Text
+ Rectangle {
+ visible: parent.labelVisible
+
+ anchors.fill: parent
+
+ // padding
+ anchors.topMargin: 10
+ anchors.bottomMargin: 10
+
+ border.color: parent.textBorderColor
+ border.width: 1
+
+ color: parent.hovered ? parent.textBgColorHovered : parent.textBgColor
+
+ radius: 25
+ }
+
+ // background with border around the Image
+ Rectangle {
+ visible: parent.iconVisible
+
+ anchors.fill: parent
+
+ color: parent.hovered ? parent.iconBgColorHovered : parent.iconBgColor
+ }
+
+ // label
+ Text {
+ id: label
+ visible: parent.text !== ""
+ text: parent.text
+ font: parent.font
+ color: parent.hovered ? parent.textColorHovered : parent.textColor
+ anchors.fill: parent
+ anchors.leftMargin: 10
+ anchors.rightMargin: 10
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ }
+
+ // icon
+ Image {
+ id: icon
+ visible: parent.imageSource !== ""
+ anchors.centerIn: parent
+ source: parent.imageSource
+ sourceSize.width: visible ? 32 : 0
+ sourceSize.height: visible ? 32 : 0
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ onClicked: parent.clicked()
+ hoverEnabled: true
+ }
+
+ ToolTip {
+ text: parent.tooltipText
+ delay: 1000
+ visible: text != "" && parent.hovered
+ }
+}
diff --git a/src/gui/tray/ActivityData.h b/src/gui/tray/ActivityData.h
index be30f77a2..f8bbef101 100644
--- a/src/gui/tray/ActivityData.h
+++ b/src/gui/tray/ActivityData.h
@@ -27,11 +27,18 @@ namespace OCC {
class ActivityLink
{
+ Q_GADGET
+
+ Q_PROPERTY(QString label MEMBER _label)
+ Q_PROPERTY(QString link MEMBER _link)
+ Q_PROPERTY(QByteArray verb MEMBER _verb)
+ Q_PROPERTY(bool primary MEMBER _primary)
+
public:
QString _label;
QString _link;
QByteArray _verb;
- bool _isPrimary;
+ bool _primary;
};
/* ==================================================================== */
diff --git a/src/gui/tray/ActivityListModel.cpp b/src/gui/tray/ActivityListModel.cpp
index 15d7a22bd..a2d39252d 100644
--- a/src/gui/tray/ActivityListModel.cpp
+++ b/src/gui/tray/ActivityListModel.cpp
@@ -53,6 +53,7 @@ QHash ActivityListModel::roleNames() const
roles[ActionRole] = "type";
roles[ActionIconRole] = "icon";
roles[ActionTextRole] = "subject";
+ roles[ActionsLinksRole] = "links";
roles[ActionTextColorRole] = "activityTextTitleColor";
roles[ObjectTypeRole] = "objectType";
roles[PointInTimeRole] = "dateTime";
@@ -139,10 +140,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
}
case ActionsLinksRole: {
QList customList;
- foreach (ActivityLink customItem, a._links) {
- QVariant customVariant;
- customVariant.setValue(customItem);
- customList << customVariant;
+ foreach (ActivityLink activityLink, a._links) {
+ customList << QVariant::fromValue(activityLink);
}
return customList;
}
@@ -418,17 +417,17 @@ void ActivityListModel::removeActivityFromActivityList(Activity activity)
}
}
-void ActivityListModel::triggerActionAtIndex(int id) const
+void ActivityListModel::triggerDefaultAction(int activityIndex) const
{
- if (id < 0 || id >= _finalList.size()) {
- qCWarning(lcActivity) << "Couldn't trigger action at index" << id << "/ final list size:" << _finalList.size();
+ if (activityIndex < 0 || activityIndex >= _finalList.size()) {
+ qCWarning(lcActivity) << "Couldn't trigger default action at index" << activityIndex << "/ final list size:" << _finalList.size();
return;
}
- const auto modelIndex = index(id);
+ const auto modelIndex = index(activityIndex);
const auto path = data(modelIndex, PathRole).toUrl();
- const auto activity = _finalList.at(id);
+ const auto activity = _finalList.at(activityIndex);
if (activity._status == SyncFileItem::Conflict) {
Q_ASSERT(!activity._file.isEmpty());
Q_ASSERT(!activity._folder.isEmpty());
@@ -461,6 +460,30 @@ void ActivityListModel::triggerActionAtIndex(int id) const
}
}
+void ActivityListModel::triggerAction(int activityIndex, int actionIndex)
+{
+ if (activityIndex < 0 || activityIndex >= _finalList.size()) {
+ qCWarning(lcActivity) << "Couldn't trigger action on activity at index" << activityIndex << "/ final list size:" << _finalList.size();
+ return;
+ }
+
+ const auto activity = _finalList[activityIndex];
+
+ if (actionIndex < 0 || actionIndex >= activity._links.size()) {
+ qCWarning(lcActivity) << "Couldn't trigger action at index" << actionIndex << "/ actions list size:" << activity._links.size();
+ return;
+ }
+
+ const auto action = activity._links[actionIndex];
+
+ if (action._verb == "WEB") {
+ QDesktopServices::openUrl(QUrl(action._link));
+ return;
+ }
+
+ emit sendNotificationRequest(activity._accName, action._link, action._verb, activityIndex);
+}
+
void ActivityListModel::combineActivityLists()
{
ActivityList resultList;
diff --git a/src/gui/tray/ActivityListModel.h b/src/gui/tray/ActivityListModel.h
index f09d39d66..29b14e921 100644
--- a/src/gui/tray/ActivityListModel.h
+++ b/src/gui/tray/ActivityListModel.h
@@ -74,7 +74,8 @@ public:
void removeActivityFromActivityList(int row);
void removeActivityFromActivityList(Activity activity);
- Q_INVOKABLE void triggerActionAtIndex(int id) const;
+ Q_INVOKABLE void triggerDefaultAction(int activityIndex) const;
+ Q_INVOKABLE void triggerAction(int activityIndex, int actionIndex);
public slots:
void slotRefreshActivity();
@@ -86,6 +87,7 @@ private slots:
signals:
void activityJobStatusCode(int statusCode);
+ void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
protected:
QHash roleNames() const override;
diff --git a/src/gui/tray/NotificationHandler.cpp b/src/gui/tray/NotificationHandler.cpp
index 445d0a88a..98e5c1aa7 100644
--- a/src/gui/tray/NotificationHandler.cpp
+++ b/src/gui/tray/NotificationHandler.cpp
@@ -128,7 +128,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
al._label = QUrl::fromPercentEncoding(actionJson.value("label").toString().toUtf8());
al._link = actionJson.value("link").toString();
al._verb = actionJson.value("type").toString().toUtf8();
- al._isPrimary = actionJson.value("primary").toBool();
+ al._primary = actionJson.value("primary").toBool();
a._links.append(al);
}
@@ -139,7 +139,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
al._label = tr("Dismiss");
al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString();
al._verb = "DELETE";
- al._isPrimary = false;
+ al._primary = false;
a._links.append(al);
list.append(a);
diff --git a/src/gui/tray/UserModel.cpp b/src/gui/tray/UserModel.cpp
index 9df3dd111..8e83bc936 100644
--- a/src/gui/tray/UserModel.cpp
+++ b/src/gui/tray/UserModel.cpp
@@ -51,6 +51,8 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
connect(this, &User::guiLog, Logger::instance(), &Logger::guiLog);
connect(_account->account().data(), &Account::accountChangedAvatar, this, &User::avatarChanged);
+
+ connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
}
void User::slotBuildNotificationDisplay(const ActivityList &list)
@@ -329,7 +331,7 @@ void User::slotAddError(const QString &folderAlias, const QString &message, Erro
link._label = tr("Retry all uploads");
link._link = folderInstance->path();
link._verb = "";
- link._isPrimary = true;
+ link._primary = true;
activity._links.append(link);
}
diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml
index a007a9cb3..ac9de333f 100644
--- a/src/gui/tray/Window.qml
+++ b/src/gui/tray/Window.qml
@@ -108,23 +108,11 @@ Window {
id: trayWindowHeaderBackground
anchors.left: trayWindowBackground.left
+ anchors.right: trayWindowBackground.right
anchors.top: trayWindowBackground.top
height: Style.trayWindowHeaderHeight
- width: Style.trayWindowWidth
color: Style.ncBlue
- // The overlay rectangle below eliminates the rounded corners from the bottom of the header
- // as Qt only allows setting the radius for all corners right now, not specific ones
- Rectangle {
- id: trayWindowHeaderButtomHalfBackground
-
- anchors.left: trayWindowHeaderBackground.left
- anchors.bottom: trayWindowHeaderBackground.bottom
- height: Style.trayWindowHeaderHeight / 2
- width: Style.trayWindowWidth
- color: Style.ncBlue
- }
-
RowLayout {
id: trayWindowHeaderLayout
@@ -511,16 +499,17 @@ Window {
ListView {
id: activityListView
-
anchors.top: trayWindowHeaderBackground.bottom
- anchors.horizontalCenter: trayWindowBackground.horizontalCenter
- width: Style.trayWindowWidth - Style.trayWindowBorderWidth
- height: Style.trayWindowHeight - Style.trayWindowHeaderHeight
+ anchors.left: trayWindowBackground.left
+ anchors.right: trayWindowBackground.right
+ anchors.bottom: trayWindowBackground.bottom
clip: true
ScrollBar.vertical: ScrollBar {
id: listViewScrollbar
}
+ readonly property int maxActionButtons: 2
+
keyNavigationEnabled: true
Accessible.role: Accessible.List
@@ -531,26 +520,28 @@ Window {
delegate: RowLayout {
id: activityItem
+ readonly property variant links: model.links
+
+ readonly property int itemIndex: model.index
+
width: parent.width
height: Style.trayWindowHeaderHeight
spacing: 0
Accessible.role: Accessible.ListItem
Accessible.name: path !== "" ? qsTr("Open %1 locally").arg(displayPath)
- : message
+ : message
Accessible.onPressAction: activityMouseArea.clicked()
MouseArea {
id: activityMouseArea
enabled: (path !== "" || link !== "")
anchors.left: activityItem.left
- anchors.right: (shareButton.visible) ? shareButton.left
- : (replyButton.visible) ? replyButton.left
- : activityItem.right
+ anchors.right: activityActionsLayout.right
height: parent.height
anchors.margins: 2
hoverEnabled: true
- onClicked: activityModel.triggerActionAtIndex(model.index)
+ onClicked: activityModel.triggerDefaultAction(model.index)
Rectangle {
anchors.fill: parent
@@ -575,13 +566,14 @@ Window {
Column {
id: activityTextColumn
anchors.left: activityIcon.right
+ anchors.right: activityActionsLayout.left
anchors.leftMargin: 8
spacing: 4
Layout.alignment: Qt.AlignLeft
Text {
id: activityTextTitle
text: (type === "Activity" || type === "Notification") ? subject : message
- width: Style.activityLabelBaseWidth + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8
+ width: parent.width
elide: Text.ElideRight
font.pixelSize: Style.topLinePixelSize
color: activityTextTitleColor
@@ -594,7 +586,7 @@ Window {
: (type === "Notification") ? message
: ""
height: (text === "") ? 0 : activityTextTitle.height
- width: Style.activityLabelBaseWidth + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8
+ width: parent.width
elide: Text.ElideRight
font.pixelSize: Style.subLinePixelSize
}
@@ -603,7 +595,7 @@ Window {
id: activityTextDateTime
text: dateTime
height: (text === "") ? 0 : activityTextTitle.height
- width: Style.activityLabelBaseWidth + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8
+ width: parent.width
elide: Text.ElideRight
font.pixelSize: Style.subLinePixelSize
color: "#808080"
@@ -624,56 +616,175 @@ Window {
}
}
}
- Button {
- id: shareButton
+ RowLayout {
+ id: activityActionsLayout
anchors.right: activityItem.right
-
- Layout.preferredWidth: (path === "") ? 0 : parent.height
- Layout.preferredHeight: parent.height
+ spacing: 0
Layout.alignment: Qt.AlignRight
- flat: true
- hoverEnabled: true
- visible: (path === "") ? false : true
- display: AbstractButton.IconOnly
- icon.source: "qrc:///client/theme/share.svg"
- icon.color: "transparent"
- background: Rectangle {
- color: parent.hovered ? Style.lightHover : "transparent"
+
+ function actionButtonIcon(actionIndex) {
+ const verb = String(model.links[actionIndex].verb);
+ if (verb === "WEB" && (model.objectType === "chat" || model.objectType === "call")) {
+ return "qrc:///client/theme/reply.svg";
+ } else if (verb === "DELETE") {
+ return "qrc:///client/theme/close.svg";
+ }
+
+ return "qrc:///client/theme/confirm.svg";
}
- ToolTip.visible: hovered
- ToolTip.delay: 1000
- ToolTip.text: qsTr("Open share dialog")
- onClicked: Systray.openShareDialog(displayPath,absolutePath)
- Accessible.role: Accessible.Button
- Accessible.name: qsTr("Share %1").arg(displayPath)
- Accessible.onPressAction: shareButton.clicked()
- }
+ Repeater {
+ model: activityItem.links.length > activityListView.maxActionButtons ? 1 : activityItem.links.length
- Button {
- id: replyButton
- anchors.right: activityItem.right
+ ActivityActionButton {
+ id: activityActionButton
+
+ readonly property int actionIndex: model.index
+ readonly property bool primary: model.index === 0 && String(activityItem.links[actionIndex].verb) !== "DELETE"
+
+ height: activityItem.height
+
+ text: !primary ? "" : activityItem.links[actionIndex].label
+
+ imageSource: !primary ? activityActionsLayout.actionButtonIcon(actionIndex) : ""
+
+ textColor: primary ? Style.ncBlue : "black"
+ textColorHovered: Style.lightHover
+
+ textBorderColor: Style.ncBlue
+
+ textBgColor: "transparent"
+ textBgColorHovered: Style.ncBlue
+
+ tooltipText: activityItem.links[actionIndex].label
+
+ Layout.minimumWidth: primary ? 80 : -1
+ Layout.minimumHeight: parent.height
+
+ Layout.preferredWidth: primary ? -1 : parent.height
+
+ onClicked: activityModel.triggerAction(activityItem.itemIndex, actionIndex)
+ }
- Layout.preferredWidth: (objectType == "chat" || objectType == "call") ? parent.height : 0
- Layout.preferredHeight: parent.height
- Layout.alignment: Qt.AlignRight
- flat: true
- hoverEnabled: true
- visible: (objectType == "chat" || objectType == "call") ? true : false
- display: AbstractButton.IconOnly
- icon.source: "qrc:///client/theme/reply.svg"
- icon.color: "transparent"
- background: Rectangle {
- color: parent.hovered ? Style.lightHover : "transparent"
}
- ToolTip.visible: hovered
- ToolTip.delay: 1000
- ToolTip.text: qsTr("Open Talk")
- onClicked: Qt.openUrlExternally(link)
- Accessible.role: Accessible.Button
- Accessible.name: qsTr("Open Talk %1").arg(link)
- Accessible.onPressAction: replyButton.clicked()
+ Button {
+ id: moreActionsButton
+
+ Layout.preferredWidth: parent.height
+ Layout.preferredHeight: parent.height
+ Layout.alignment: Qt.AlignRight
+
+ flat: true
+ hoverEnabled: true
+ visible: activityItem.links.length > activityListView.maxActionButtons
+ display: AbstractButton.IconOnly
+ icon.source: "qrc:///client/theme/more.svg"
+ icon.color: "transparent"
+ background: Rectangle {
+ color: parent.hovered ? Style.lightHover : "transparent"
+ }
+ ToolTip.visible: hovered
+ ToolTip.delay: 1000
+ ToolTip.text: qsTr("Show more actions")
+
+ Accessible.role: Accessible.Button
+ Accessible.name: qsTr("Show more actions")
+ Accessible.onPressAction: moreActionsButton.clicked()
+
+ onClicked: moreActionsButtonContextMenu.popup();
+
+ Connections {
+ target: trayWindow
+ onActiveChanged: {
+ if (!trayWindow.active) {
+ moreActionsButtonContextMenu.close();
+ }
+ }
+ }
+
+ Connections {
+ target: activityListView
+
+ onMovementStarted: {
+ moreActionsButtonContextMenu.close();
+ }
+ }
+
+ Container {
+ id: moreActionsButtonContextMenuContainer
+ visible: moreActionsButtonContextMenu.opened
+
+ width: moreActionsButtonContextMenu.width
+ height: moreActionsButtonContextMenu.height
+ anchors.right: moreActionsButton.right
+ anchors.top: moreActionsButton.top
+
+ Menu {
+ id: moreActionsButtonContextMenu
+ anchors.centerIn: parent
+
+ // transform model to contain indexed actions with primary action filtered out
+ function actionListToContextMenuList(actionList) {
+ // early out with non-altered data
+ if (activityItem.links.length <= activityListView.maxActionButtons) {
+ return actionList;
+ }
+
+ // add index to every action and filter 'primary' action out
+ var reducedActionList = actionList.reduce(function(reduced, action, index) {
+ if (!action.primary) {
+ var actionWithIndex = { actionIndex: index, label: action.label };
+ reduced.push(actionWithIndex);
+ }
+ return reduced;
+ }, []);
+
+
+ return reducedActionList;
+ }
+
+ Repeater {
+ id: moreActionsButtonContextMenuRepeater
+
+ model: moreActionsButtonContextMenu.actionListToContextMenuList(activityItem.links)
+
+ delegate: MenuItem {
+ id: moreActionsButtonContextMenuEntry
+ readonly property int actionIndex: model.modelData.actionIndex
+ readonly property string label: model.modelData.label
+ text: label
+ onTriggered: activityModel.triggerAction(activityItem.itemIndex, actionIndex)
+ }
+ }
+ }
+ }
+ }
+
+ Button {
+ id: shareButton
+
+ Layout.preferredWidth: (path === "") ? 0 : parent.height
+ Layout.preferredHeight: parent.height
+ Layout.alignment: Qt.AlignRight
+ flat: true
+ hoverEnabled: true
+ visible: (path === "") ? false : true
+ display: AbstractButton.IconOnly
+ icon.source: "qrc:///client/theme/share.svg"
+ icon.color: "transparent"
+ background: Rectangle {
+ color: parent.hovered ? Style.lightHover : "transparent"
+ }
+ ToolTip.visible: hovered
+ ToolTip.delay: 1000
+ ToolTip.text: qsTr("Open share dialog")
+ onClicked: Systray.openShareDialog(displayPath,absolutePath)
+
+ Accessible.role: Accessible.Button
+ Accessible.name: qsTr("Share %1").arg(displayPath)
+ Accessible.onPressAction: shareButton.clicked()
+ }
}
}