Implemented notification action buttons and context menu for confirm/dismiss and other actions.

Signed-off-by: allexzander <blackslayer4@gmail.com>
This commit is contained in:
allexzander 2020-11-18 17:42:37 +02:00 committed by Kevin Ottens (Rebase PR Action)
parent 7721832ee0
commit f04281cb69
9 changed files with 334 additions and 81 deletions

View File

@ -5,5 +5,6 @@
<file>src/gui/tray/HeaderButton.qml</file> <file>src/gui/tray/HeaderButton.qml</file>
<file>theme/Style/Style.qml</file> <file>theme/Style/Style.qml</file>
<file>theme/Style/qmldir</file> <file>theme/Style/qmldir</file>
<file>src/gui/tray/ActivityActionButton.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -43,6 +43,7 @@ set(client_UI_SRCS
addcertificatedialog.ui addcertificatedialog.ui
proxyauthdialog.ui proxyauthdialog.ui
mnemonicdialog.ui mnemonicdialog.ui
tray/ActivityActionButton.qml
tray/Window.qml tray/Window.qml
tray/UserLine.qml tray/UserLine.qml
wizard/flow2authwidget.ui wizard/flow2authwidget.ui

View File

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

View File

@ -27,11 +27,18 @@ namespace OCC {
class ActivityLink 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: public:
QString _label; QString _label;
QString _link; QString _link;
QByteArray _verb; QByteArray _verb;
bool _isPrimary; bool _primary;
}; };
/* ==================================================================== */ /* ==================================================================== */

View File

@ -53,6 +53,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
roles[ActionRole] = "type"; roles[ActionRole] = "type";
roles[ActionIconRole] = "icon"; roles[ActionIconRole] = "icon";
roles[ActionTextRole] = "subject"; roles[ActionTextRole] = "subject";
roles[ActionsLinksRole] = "links";
roles[ActionTextColorRole] = "activityTextTitleColor"; roles[ActionTextColorRole] = "activityTextTitleColor";
roles[ObjectTypeRole] = "objectType"; roles[ObjectTypeRole] = "objectType";
roles[PointInTimeRole] = "dateTime"; roles[PointInTimeRole] = "dateTime";
@ -139,10 +140,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
} }
case ActionsLinksRole: { case ActionsLinksRole: {
QList<QVariant> customList; QList<QVariant> customList;
foreach (ActivityLink customItem, a._links) { foreach (ActivityLink activityLink, a._links) {
QVariant customVariant; customList << QVariant::fromValue(activityLink);
customVariant.setValue(customItem);
customList << customVariant;
} }
return customList; 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()) { if (activityIndex < 0 || activityIndex >= _finalList.size()) {
qCWarning(lcActivity) << "Couldn't trigger action at index" << id << "/ final list size:" << _finalList.size(); qCWarning(lcActivity) << "Couldn't trigger default action at index" << activityIndex << "/ final list size:" << _finalList.size();
return; return;
} }
const auto modelIndex = index(id); const auto modelIndex = index(activityIndex);
const auto path = data(modelIndex, PathRole).toUrl(); const auto path = data(modelIndex, PathRole).toUrl();
const auto activity = _finalList.at(id); const auto activity = _finalList.at(activityIndex);
if (activity._status == SyncFileItem::Conflict) { if (activity._status == SyncFileItem::Conflict) {
Q_ASSERT(!activity._file.isEmpty()); Q_ASSERT(!activity._file.isEmpty());
Q_ASSERT(!activity._folder.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() void ActivityListModel::combineActivityLists()
{ {
ActivityList resultList; ActivityList resultList;

View File

@ -74,7 +74,8 @@ public:
void removeActivityFromActivityList(int row); void removeActivityFromActivityList(int row);
void removeActivityFromActivityList(Activity activity); 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: public slots:
void slotRefreshActivity(); void slotRefreshActivity();
@ -86,6 +87,7 @@ private slots:
signals: signals:
void activityJobStatusCode(int statusCode); void activityJobStatusCode(int statusCode);
void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
protected: protected:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;

View File

@ -128,7 +128,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
al._label = QUrl::fromPercentEncoding(actionJson.value("label").toString().toUtf8()); al._label = QUrl::fromPercentEncoding(actionJson.value("label").toString().toUtf8());
al._link = actionJson.value("link").toString(); al._link = actionJson.value("link").toString();
al._verb = actionJson.value("type").toString().toUtf8(); al._verb = actionJson.value("type").toString().toUtf8();
al._isPrimary = actionJson.value("primary").toBool(); al._primary = actionJson.value("primary").toBool();
a._links.append(al); a._links.append(al);
} }
@ -139,7 +139,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
al._label = tr("Dismiss"); al._label = tr("Dismiss");
al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString(); al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString();
al._verb = "DELETE"; al._verb = "DELETE";
al._isPrimary = false; al._primary = false;
a._links.append(al); a._links.append(al);
list.append(a); list.append(a);

View File

@ -51,6 +51,8 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
connect(this, &User::guiLog, Logger::instance(), &Logger::guiLog); connect(this, &User::guiLog, Logger::instance(), &Logger::guiLog);
connect(_account->account().data(), &Account::accountChangedAvatar, this, &User::avatarChanged); connect(_account->account().data(), &Account::accountChangedAvatar, this, &User::avatarChanged);
connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
} }
void User::slotBuildNotificationDisplay(const ActivityList &list) 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._label = tr("Retry all uploads");
link._link = folderInstance->path(); link._link = folderInstance->path();
link._verb = ""; link._verb = "";
link._isPrimary = true; link._primary = true;
activity._links.append(link); activity._links.append(link);
} }

View File

@ -108,23 +108,11 @@ Window {
id: trayWindowHeaderBackground id: trayWindowHeaderBackground
anchors.left: trayWindowBackground.left anchors.left: trayWindowBackground.left
anchors.right: trayWindowBackground.right
anchors.top: trayWindowBackground.top anchors.top: trayWindowBackground.top
height: Style.trayWindowHeaderHeight height: Style.trayWindowHeaderHeight
width: Style.trayWindowWidth
color: Style.ncBlue 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 { RowLayout {
id: trayWindowHeaderLayout id: trayWindowHeaderLayout
@ -511,16 +499,17 @@ Window {
ListView { ListView {
id: activityListView id: activityListView
anchors.top: trayWindowHeaderBackground.bottom anchors.top: trayWindowHeaderBackground.bottom
anchors.horizontalCenter: trayWindowBackground.horizontalCenter anchors.left: trayWindowBackground.left
width: Style.trayWindowWidth - Style.trayWindowBorderWidth anchors.right: trayWindowBackground.right
height: Style.trayWindowHeight - Style.trayWindowHeaderHeight anchors.bottom: trayWindowBackground.bottom
clip: true clip: true
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
id: listViewScrollbar id: listViewScrollbar
} }
readonly property int maxActionButtons: 2
keyNavigationEnabled: true keyNavigationEnabled: true
Accessible.role: Accessible.List Accessible.role: Accessible.List
@ -531,26 +520,28 @@ Window {
delegate: RowLayout { delegate: RowLayout {
id: activityItem id: activityItem
readonly property variant links: model.links
readonly property int itemIndex: model.index
width: parent.width width: parent.width
height: Style.trayWindowHeaderHeight height: Style.trayWindowHeaderHeight
spacing: 0 spacing: 0
Accessible.role: Accessible.ListItem Accessible.role: Accessible.ListItem
Accessible.name: path !== "" ? qsTr("Open %1 locally").arg(displayPath) Accessible.name: path !== "" ? qsTr("Open %1 locally").arg(displayPath)
: message : message
Accessible.onPressAction: activityMouseArea.clicked() Accessible.onPressAction: activityMouseArea.clicked()
MouseArea { MouseArea {
id: activityMouseArea id: activityMouseArea
enabled: (path !== "" || link !== "") enabled: (path !== "" || link !== "")
anchors.left: activityItem.left anchors.left: activityItem.left
anchors.right: (shareButton.visible) ? shareButton.left anchors.right: activityActionsLayout.right
: (replyButton.visible) ? replyButton.left
: activityItem.right
height: parent.height height: parent.height
anchors.margins: 2 anchors.margins: 2
hoverEnabled: true hoverEnabled: true
onClicked: activityModel.triggerActionAtIndex(model.index) onClicked: activityModel.triggerDefaultAction(model.index)
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@ -575,13 +566,14 @@ Window {
Column { Column {
id: activityTextColumn id: activityTextColumn
anchors.left: activityIcon.right anchors.left: activityIcon.right
anchors.right: activityActionsLayout.left
anchors.leftMargin: 8 anchors.leftMargin: 8
spacing: 4 spacing: 4
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Text { Text {
id: activityTextTitle id: activityTextTitle
text: (type === "Activity" || type === "Notification") ? subject : message 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 elide: Text.ElideRight
font.pixelSize: Style.topLinePixelSize font.pixelSize: Style.topLinePixelSize
color: activityTextTitleColor color: activityTextTitleColor
@ -594,7 +586,7 @@ Window {
: (type === "Notification") ? message : (type === "Notification") ? message
: "" : ""
height: (text === "") ? 0 : activityTextTitle.height height: (text === "") ? 0 : activityTextTitle.height
width: Style.activityLabelBaseWidth + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8 width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
font.pixelSize: Style.subLinePixelSize font.pixelSize: Style.subLinePixelSize
} }
@ -603,7 +595,7 @@ Window {
id: activityTextDateTime id: activityTextDateTime
text: dateTime text: dateTime
height: (text === "") ? 0 : activityTextTitle.height height: (text === "") ? 0 : activityTextTitle.height
width: Style.activityLabelBaseWidth + ((path === "") ? activityItem.height : 0) + ((link === "") ? activityItem.height : 0) - 8 width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
font.pixelSize: Style.subLinePixelSize font.pixelSize: Style.subLinePixelSize
color: "#808080" color: "#808080"
@ -624,56 +616,175 @@ Window {
} }
} }
} }
Button { RowLayout {
id: shareButton id: activityActionsLayout
anchors.right: activityItem.right anchors.right: activityItem.right
spacing: 0
Layout.preferredWidth: (path === "") ? 0 : parent.height
Layout.preferredHeight: parent.height
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
flat: true
hoverEnabled: true function actionButtonIcon(actionIndex) {
visible: (path === "") ? false : true const verb = String(model.links[actionIndex].verb);
display: AbstractButton.IconOnly if (verb === "WEB" && (model.objectType === "chat" || model.objectType === "call")) {
icon.source: "qrc:///client/theme/share.svg" return "qrc:///client/theme/reply.svg";
icon.color: "transparent" } else if (verb === "DELETE") {
background: Rectangle { return "qrc:///client/theme/close.svg";
color: parent.hovered ? Style.lightHover : "transparent" }
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 Repeater {
Accessible.name: qsTr("Share %1").arg(displayPath) model: activityItem.links.length > activityListView.maxActionButtons ? 1 : activityItem.links.length
Accessible.onPressAction: shareButton.clicked()
}
Button { ActivityActionButton {
id: replyButton id: activityActionButton
anchors.right: activityItem.right
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 Button {
Accessible.name: qsTr("Open Talk %1").arg(link) id: moreActionsButton
Accessible.onPressAction: replyButton.clicked()
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()
}
} }
} }