diff options
author | alex-z <blackslayer4@gmail.com> | 2022-01-04 17:28:26 +0300 |
---|---|---|
committer | alex-z <blackslayer4@gmail.com> | 2022-02-04 18:52:37 +0300 |
commit | ae44dd59787cd4614db1c11fe1376035345f36da (patch) | |
tree | f40cab03c5eca7410f7429bb5502ed09ab4255d7 /src/gui | |
parent | 5ce8c9bf50f7a9e8753e0fddf471bbc8eb7d6ccc (diff) |
Adjust icons for activity entries in main dialog. Refactor the dialog by splitting it to separate components.
Signed-off-by: alex-z <blackslayer4@gmail.com>
Diffstat (limited to 'src/gui')
-rw-r--r-- | src/gui/tray/ActivityActionButton.qml | 116 | ||||
-rw-r--r-- | src/gui/tray/ActivityItem.qml | 298 | ||||
-rw-r--r-- | src/gui/tray/ActivityItemActions.qml | 103 | ||||
-rw-r--r-- | src/gui/tray/ActivityItemContent.qml | 127 | ||||
-rw-r--r-- | src/gui/tray/ActivityItemContextMenu.qml | 25 | ||||
-rw-r--r-- | src/gui/tray/ActivityList.qml | 17 | ||||
-rw-r--r-- | src/gui/tray/CustomButton.qml | 61 | ||||
-rw-r--r-- | src/gui/tray/CustomTextButton.qml | 49 | ||||
-rw-r--r-- | src/gui/tray/FileActivityDialog.qml | 1 | ||||
-rw-r--r-- | src/gui/tray/Window.qml | 2 | ||||
-rw-r--r-- | src/gui/tray/activitydata.cpp | 11 | ||||
-rw-r--r-- | src/gui/tray/activitydata.h | 12 | ||||
-rw-r--r-- | src/gui/tray/activitylistmodel.cpp | 166 | ||||
-rw-r--r-- | src/gui/tray/activitylistmodel.h | 29 | ||||
-rw-r--r-- | src/gui/tray/notificationhandler.cpp | 9 | ||||
-rw-r--r-- | src/gui/tray/usermodel.cpp | 1 |
16 files changed, 676 insertions, 351 deletions
diff --git a/src/gui/tray/ActivityActionButton.qml b/src/gui/tray/ActivityActionButton.qml index 6cf3d3495..4c64e0bc8 100644 --- a/src/gui/tray/ActivityActionButton.qml +++ b/src/gui/tray/ActivityActionButton.qml @@ -1,109 +1,65 @@ -import QtQuick 2.5 +import QtQuick 2.15 import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.15 import Style 1.0 Item { id: root - readonly property bool labelVisible: label.visible - readonly property bool iconVisible: icon.visible - // label value property string text: "" - - // font value - property var font: label.font + property string toolTipText: "" - // 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 + property bool bold: false - // icon background color - property color iconBgColor: "transparent" - property color iconBgColorHovered: Style.lightHover - - // text border color - property color textBorderColor: "transparent" + property string imageSource: "" + property string imageSourceHover: "" - property alias hovered: mouseArea.containsMouse + property color textColor: Style.unifiedSearchResulTitleColor + property color textColorHovered: Style.unifiedSearchResulSublineColor 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 + Loader { + active: root.imageSource === "" anchors.fill: parent - // padding - anchors.topMargin: 10 - anchors.bottomMargin: 10 - - border.color: parent.textBorderColor - border.width: 1 + sourceComponent: CustomTextButton { + anchors.fill: parent + text: root.text + toolTipText: root.toolTipText - color: parent.hovered ? parent.textBgColorHovered : parent.textBgColor + textColor: root.textColor + textColorHovered: root.textColorHovered - radius: 25 + onClicked: root.clicked() + } } - // background with border around the Image - Rectangle { - visible: parent.iconVisible + Loader { + active: root.imageSource !== "" anchors.fill: parent - color: parent.hovered ? parent.iconBgColorHovered : parent.iconBgColor - } + sourceComponent: CustomButton { + anchors.fill: parent + anchors.topMargin: Style.roundedButtonBackgroundVerticalMargins + anchors.bottomMargin: Style.roundedButtonBackgroundVerticalMargins - // 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 - } + text: root.text + toolTipText: root.toolTipText - // icon - Image { - id: icon - visible: parent.imageSource !== "" - anchors.centerIn: parent - source: parent.imageSource - sourceSize.width: visible ? 32 : 0 - sourceSize.height: visible ? 32 : 0 - } + textColor: root.textColor + textColorHovered: root.textColorHovered - MouseArea { - id: mouseArea - anchors.fill: parent - onClicked: parent.clicked() - hoverEnabled: true - } + bold: root.bold + + imageSource: root.imageSource + imageSourceHover: root.imageSourceHover + + bgColor: Style.ncBlue - ToolTip { - text: parent.tooltipText - delay: 1000 - visible: text != "" && parent.hovered + onClicked: root.clicked() + } } } diff --git a/src/gui/tray/ActivityItem.qml b/src/gui/tray/ActivityItem.qml index 598ae3b76..54c272cfb 100644 --- a/src/gui/tray/ActivityItem.qml +++ b/src/gui/tray/ActivityItem.qml @@ -1,264 +1,84 @@ -import QtQml 2.12 -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.2 +import QtQml 2.15 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import Style 1.0 import com.nextcloud.desktopclient 1.0 MouseArea { - id: activityMouseArea + id: root - readonly property int maxActionButtons: 2 property Flickable flickable + property bool isFileActivityList: false + + property bool isChatActivity: model.objectType === "chat" || model.objectType === "room" + signal fileActivityButtonClicked(string absolutePath) - enabled: (path !== "" || link !== "") + enabled: (model.path !== "" || model.link !== "" || model.isCurrentUserFileActivity === true) hoverEnabled: true + height: childrenRect.height + + ToolTip.visible: containsMouse && !activityContent.childHovered && model.displayLocation !== "" + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: qsTr("In %1").arg(model.displayLocation) + + Accessible.role: Accessible.ListItem + Accessible.name: (model.path !== "" && model.displayPath !== "") ? qsTr("Open %1 locally").arg(model.displayPath) : model.message + Accessible.onPressAction: root.clicked() + Rectangle { + id: activityHover anchors.fill: parent color: (parent.containsMouse ? Style.lightHover : "transparent") } - ToolTip.visible: containsMouse && displayLocation !== "" - ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval - ToolTip.text: qsTr("In %1").arg(displayLocation) - - RowLayout { - id: activityItem - - readonly property variant links: model.links - - readonly property int itemIndex: model.index - - width: activityMouseArea.width - height: Style.trayWindowHeaderHeight + ColumnLayout { + anchors.left: root.left + anchors.right: root.right + anchors.leftMargin: 15 + anchors.rightMargin: 10 + spacing: 0 - - Accessible.role: Accessible.ListItem - Accessible.name: path !== "" ? qsTr("Open %1 locally").arg(displayPath) - : message - Accessible.onPressAction: activityMouseArea.clicked() - - Image { - id: activityIcon - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.leftMargin: 20 - Layout.preferredWidth: shareButton.icon.width - Layout.preferredHeight: shareButton.icon.height - verticalAlignment: Qt.AlignCenter - cache: true - source: icon - sourceSize.height: 64 - sourceSize.width: 64 - } - - Column { - id: activityTextColumn - Layout.leftMargin: 14 - Layout.topMargin: 4 - Layout.bottomMargin: 4 + + ActivityItemContent { + id: activityContent + Layout.fillWidth: true - spacing: 4 - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - - Text { - id: activityTextTitle - text: (type === "Activity" || type === "Notification") ? subject : message - width: parent.width - elide: Text.ElideRight - font.pixelSize: Style.topLinePixelSize - color: activityTextTitleColor - } - - Text { - id: activityTextInfo - text: (type === "Sync") ? displayPath - : (type === "File") ? subject - : (type === "Notification") ? message - : "" - height: (text === "") ? 0 : activityTextTitle.height - width: parent.width - elide: Text.ElideRight - font.pixelSize: Style.subLinePixelSize - } - - Text { - id: activityTextDateTime - text: dateTime - height: (text === "") ? 0 : activityTextTitle.height - width: parent.width - elide: Text.ElideRight - font.pixelSize: Style.subLinePixelSize - color: "#808080" - } + + showDismissButton: model.links.length > 0 && model.linksForActionButtons.length === 0 + + activityData: model + + Layout.preferredHeight: Style.trayWindowHeaderHeight + + onShareButtonClicked: Systray.openShareDialog(model.displayPath, model.absolutePath) + onDismissButtonClicked: activityModel.slotTriggerDismiss(model.index) } - - RowLayout { - id: activityActionsLayout - spacing: 0 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.minimumWidth: 28 + + ActivityItemActions { + id: activityActions + + visible: !root.isFileActivityList && model.linksForActionButtons.length > 0 + + Layout.preferredHeight: Style.trayWindowHeaderHeight * 0.85 Layout.fillWidth: true - - 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"; - } - - Repeater { - model: activityItem.links.length > maxActionButtons ? 1 : activityItem.links.length - - ActivityActionButton { - id: activityActionButton - - readonly property int actionIndex: model.index - readonly property bool primary: model.index === 0 && String(activityItem.links[actionIndex].verb) !== "DELETE" - - Layout.fillHeight: true - - 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.leftMargin: 40 + Layout.bottomMargin: model.links.length > 1 ? 5 : 0 + + displayActions: model.displayActions + objectType: model.objectType + linksForActionButtons: model.linksForActionButtons + linksContextMenu: model.linksContextMenu + + moreActionsButtonColor: activityHover.color + maxActionButtons: activityModel.maxActionButtons - Button { - id: shareButton - - Layout.preferredWidth: parent.height - Layout.fillHeight: true - Layout.alignment: Qt.AlignRight - flat: true - hoverEnabled: true - visible: isShareable - 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: Qt.styleHints.mousePressAndHoldInterval - 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() - } - - Button { - id: moreActionsButton - - Layout.preferredWidth: parent.height - Layout.preferredHeight: parent.height - Layout.alignment: Qt.AlignRight - - flat: true - hoverEnabled: true - visible: displayActions && ((path !== "") || (activityItem.links.length > 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: Qt.styleHints.mousePressAndHoldInterval - 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: flickable - - function onMovementStarted() { - moreActionsButtonContextMenu.close(); - } - } - - Container { - id: moreActionsButtonContextMenuContainer - visible: moreActionsButtonContextMenu.opened - - width: moreActionsButtonContextMenu.width - height: moreActionsButtonContextMenu.height - anchors.right: moreActionsButton.right - anchors.top: moreActionsButton.top - - AutoSizingMenu { - 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 <= 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; - } + flickable: root.flickable - MenuItem { - text: qsTr("View activity") - onClicked: fileActivityButtonClicked(absolutePath) - } - - Repeater { - id: moreActionsButtonContextMenuRepeater - - model: moreActionsButtonContextMenu.actionListToContextMenuList(activityItem.links) - - delegate: MenuItem { - id: moreActionsButtonContextMenuEntry - text: model.modelData.label - onTriggered: activityModel.triggerAction(activityItem.itemIndex, model.modelData.actionIndex) - } - } - } - } - } + onTriggerAction: activityModel.slotTriggerAction(model.index, actionIndex) } } } diff --git a/src/gui/tray/ActivityItemActions.qml b/src/gui/tray/ActivityItemActions.qml new file mode 100644 index 000000000..42875b9d3 --- /dev/null +++ b/src/gui/tray/ActivityItemActions.qml @@ -0,0 +1,103 @@ +import QtQml 2.15 +import QtQuick 2.15 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import Style 1.0 + +RowLayout { + id: root + + spacing: 20 + + property string objectType: "" + property variant linksForActionButtons: [] + property variant linksContextMenu: [] + property bool displayActions: false + + property color moreActionsButtonColor: "transparent" + + property int maxActionButtons: 0 + + property Flickable flickable + + signal triggerAction(int actionIndex) + + Repeater { + id: actionsRepeater + // a max of maxActionButtons will get dispayed as separate buttons + model: root.linksForActionButtons + + ActivityActionButton { + id: activityActionButton + + readonly property bool primary: model.index === 0 && model.modelData.verb !== "DELETE" + + Layout.minimumWidth: primary ? Style.activityItemActionPrimaryButtonMinWidth : Style.activityItemActionSecondaryButtonMinWidth + Layout.preferredHeight: primary ? parent.height : parent.height * 0.3 + Layout.preferredWidth: primary ? -1 : parent.height + + text: model.modelData.label + toolTipText: model.modelData.label + + imageSource: model.modelData.imageSource + imageSourceHover: model.modelData.imageSourceHovered + + textColor: imageSource !== "" ? Style.ncBlue : Style.unifiedSearchResulSublineColor + textColorHovered: imageSource !== "" ? Style.lightHover : Style.unifiedSearchResulTitleColor + + bold: primary + + onClicked: root.triggerAction(model.index) + } + } + + Loader { + // actions that do not fit maxActionButtons limit, must be put into a context menu + id: moreActionsButtonContainer + + Layout.preferredWidth: parent.height + Layout.topMargin: Style.roundedButtonBackgroundVerticalMargins + Layout.bottomMargin: Style.roundedButtonBackgroundVerticalMargins + Layout.fillHeight: true + + active: root.displayActions && (root.linksContextMenu.length > 0) + + sourceComponent: Button { + id: moreActionsButton + + icon.source: "qrc:///client/theme/more.svg" + + background: Rectangle { + color: parent.hovered ? "white" : root.moreActionsButtonColor + radius: width / 2 + } + + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: qsTr("Show more actions") + + Accessible.name: qsTr("Show more actions") + + onClicked: moreActionsButtonContextMenu.popup(moreActionsButton.x, moreActionsButton.y); + + Connections { + target: root.flickable + + function onMovementStarted() { + moreActionsButtonContextMenu.close(); + } + } + + ActivityItemContextMenu { + id: moreActionsButtonContextMenu + + maxActionButtons: root.maxActionButtons + linksContextMenu: root.linksContextMenu + + onMenuEntryTriggered: function(entryIndex) { + root.triggerAction(entryIndex) + } + } + } + } +} diff --git a/src/gui/tray/ActivityItemContent.qml b/src/gui/tray/ActivityItemContent.qml new file mode 100644 index 000000000..c5057d77d --- /dev/null +++ b/src/gui/tray/ActivityItemContent.qml @@ -0,0 +1,127 @@ +import QtQml 2.15 +import QtQuick 2.15 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import Style 1.0 +import com.nextcloud.desktopclient 1.0 + +RowLayout { + id: root + + property variant activityData: {{}} + + property color activityTextTitleColor: Style.ncTextColor + + property bool showDismissButton: false + + property bool childHovered: shareButton.hovered || dismissActionButton.hovered + + signal dismissButtonClicked() + signal shareButtonClicked() + + spacing: 10 + + Image { + id: activityIcon + + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + + verticalAlignment: Qt.AlignCenter + source: icon + sourceSize.height: 64 + sourceSize.width: 64 + } + + Column { + id: activityTextColumn + + Layout.topMargin: 4 + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + spacing: 4 + + Label { + id: activityTextTitle + text: (root.activityData.type === "Activity" || root.activityData.type === "Notification") ? root.activityData.subject : root.activityData.message + width: parent.width + elide: Text.ElideRight + font.pixelSize: Style.topLinePixelSize + color: root.activityData.activityTextTitleColor + } + + Label { + id: activityTextInfo + text: (root.activityData.type === "Sync") ? root.activityData.displayPath + : (root.activityData.type === "File") ? root.activityData.subject + : (root.activityData.type === "Notification") ? root.activityData.message + : "" + height: (text === "") ? 0 : activityTextTitle.height + width: parent.width + elide: Text.ElideRight + font.pixelSize: Style.subLinePixelSize + } + + Label { + id: activityTextDateTime + text: root.activityData.dateTime + height: (text === "") ? 0 : activityTextTitle.height + width: parent.width + elide: Text.ElideRight + font.pixelSize: Style.subLinePixelSize + color: "#808080" + } + } + + Button { + id: dismissActionButton + + Layout.preferredWidth: parent.height * 0.40 + Layout.preferredHeight: parent.height * 0.40 + + Layout.alignment: Qt.AlignCenter + + Layout.margins: Style.roundButtonBackgroundVerticalMargins + + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: qsTr("Dismiss") + + Accessible.name: qsTr("Dismiss") + + visible: root.showDismissButton && !shareButton.visible + + background: Rectangle { + color: "transparent" + } + + contentItem: Image { + anchors.fill: parent + source: parent.hovered ? "image://svgimage-custom-color/clear.svg/black" : "image://svgimage-custom-color/clear.svg/grey" + sourceSize.width: 24 + sourceSize.height: 24 + } + + onClicked: root.dismissButtonClicked() + } + + CustomButton { + id: shareButton + + Layout.preferredWidth: parent.height * 0.70 + Layout.preferredHeight: parent.height * 0.70 + + visible: root.activityData.isShareable + + imageSource: "image://svgimage-custom-color/share.svg" + "/" + Style.ncBlue + imageSourceHover: "image://svgimage-custom-color/share.svg" + "/" + Style.ncTextColor + + toolTipText: qsTr("Open share dialog") + + bgColor: Style.ncBlue + + onClicked: root.shareButtonClicked() + } +} diff --git a/src/gui/tray/ActivityItemContextMenu.qml b/src/gui/tray/ActivityItemContextMenu.qml new file mode 100644 index 000000000..acf9b4267 --- /dev/null +++ b/src/gui/tray/ActivityItemContextMenu.qml @@ -0,0 +1,25 @@ +import QtQml 2.15 +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +AutoSizingMenu { + id: moreActionsButtonContextMenu + + property int maxActionButtons: 0 + + property var linksContextMenu: [] + + signal menuEntryTriggered(int index) + + Repeater { + id: moreActionsButtonContextMenuRepeater + + model: moreActionsButtonContextMenu.linksContextMenu + + delegate: MenuItem { + id: moreActionsButtonContextMenuEntry + text: model.modelData.label + onTriggered: menuEntryTriggered(model.modelData.actionIndex) + } + } +} diff --git a/src/gui/tray/ActivityList.qml b/src/gui/tray/ActivityList.qml index e1ab4f818..ced4ad2d6 100644 --- a/src/gui/tray/ActivityList.qml +++ b/src/gui/tray/ActivityList.qml @@ -1,14 +1,14 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 -import Style 1.0 - import com.nextcloud.desktopclient 1.0 as NC ScrollView { id: controlRoot property alias model: activityList.model + property bool isFileActivityList: false + signal showFileActivity(string displayPath, string absolutePath) signal activityItemClicked(int index) @@ -31,12 +31,19 @@ ScrollView { clip: true + spacing: 10 + delegate: ActivityItem { + isFileActivityList: controlRoot.isFileActivityList width: activityList.contentWidth - height: Style.trayWindowHeaderHeight flickable: activityList - onClicked: activityItemClicked(model.index) - onFileActivityButtonClicked: showFileActivity(displayPath, absolutePath) + onClicked: { + if (model.isCurrentUserFileActivity) { + showFileActivity(model.displayPath, model.absolutePath) + } else { + activityItemClicked(model.index) + } + } } } } diff --git a/src/gui/tray/CustomButton.qml b/src/gui/tray/CustomButton.qml new file mode 100644 index 000000000..7e73740bd --- /dev/null +++ b/src/gui/tray/CustomButton.qml @@ -0,0 +1,61 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 + +Button { + id: root + + property string imageSource: "" + property string imageSourceHover: "" + + property string toolTipText: "" + + property color textColor + property color textColorHovered + + property color bgColor: "transparent" + + property bool bold: false + + background: Rectangle { + color: root.bgColor + opacity: parent.hovered ? 1.0 : 0.3 + radius: width / 2 + } + + leftPadding: root.text === "" ? 5 : 10 + rightPadding: root.text === "" ? 5 : 10 + + contentItem: RowLayout { + Image { + id: icon + + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + source: root.hovered ? root.imageSourceHover : root.imageSource + } + + Label { + Layout.maximumWidth: icon.width > 0 ? parent.width - icon.width - parent.spacing : parent.width + Layout.fillWidth: icon.status !== Image.Ready + + text: root.text + font.bold: root.bold + + visible: root.text !== "" + + color: root.hovered ? root.textColorHovered : root.textColor + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + elide: Text.ElideRight + } + } + + ToolTip { + text: root.toolTipText + delay: Qt.styleHints.mousePressAndHoldInterval + visible: root.toolTipText !== "" && root.hovered + } +} diff --git a/src/gui/tray/CustomTextButton.qml b/src/gui/tray/CustomTextButton.qml new file mode 100644 index 000000000..86323a1c0 --- /dev/null +++ b/src/gui/tray/CustomTextButton.qml @@ -0,0 +1,49 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.3 +import Style 1.0 + +Label { + id: root + + property string toolTipText: "" + property Action action: null + property alias acceptedButtons: mouseArea.acceptedButtons + property bool hovered: mouseArea.containsMouse + + height: implicitHeight + + property color textColor: Style.unifiedSearchResulTitleColor + property color textColorHovered: Style.unifiedSearchResulSublineColor + + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.onPressAction: root.clicked(null) + + text: action ? action.text : "" + enabled: !action || action.enabled + onClicked: if (action) action.trigger() + + font.underline: true + color: root.hovered ? root.textColorHovered : root.textColor + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + + signal pressed(QtObject mouse) + signal clicked(QtObject mouse) + + ToolTip { + text: root.toolTipText + delay: Qt.styleHints.mousePressAndHoldInterval + visible: root.toolTipText !== "" && root.hovered + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + + onClicked: root.clicked(mouse) + onPressed: root.pressed(mouse) + } +} diff --git a/src/gui/tray/FileActivityDialog.qml b/src/gui/tray/FileActivityDialog.qml index 7fcd653d1..50452b0f3 100644 --- a/src/gui/tray/FileActivityDialog.qml +++ b/src/gui/tray/FileActivityDialog.qml @@ -15,6 +15,7 @@ Window { height: 500 ActivityList { + isFileActivityList: true anchors.fill: parent model: dialog.model } diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml index bf4ba5a9a..262e55920 100644 --- a/src/gui/tray/Window.qml +++ b/src/gui/tray/Window.qml @@ -748,7 +748,7 @@ Window { openFileActivityDialog(displayPath, absolutePath)
}
onActivityItemClicked: {
- model.triggerDefaultAction(index)
+ model.slotTriggerDefaultAction(index)
}
}
diff --git a/src/gui/tray/activitydata.cpp b/src/gui/tray/activitydata.cpp index 866c97956..16f1f1c6d 100644 --- a/src/gui/tray/activitydata.cpp +++ b/src/gui/tray/activitydata.cpp @@ -33,4 +33,15 @@ Activity::Identifier Activity::ident() const { return Identifier(_id, _accName); } + +ActivityLink ActivityLink::createFomJsonObject(const QJsonObject &obj) +{ + ActivityLink activityLink; + activityLink._label = QUrl::fromPercentEncoding(obj.value(QStringLiteral("label")).toString().toUtf8()); + activityLink._link = obj.value(QStringLiteral("link")).toString(); + activityLink._verb = obj.value(QStringLiteral("type")).toString().toUtf8(); + activityLink._primary = obj.value(QStringLiteral("primary")).toBool(); + + return activityLink; +} } diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h index 9b7c2ad98..31114e115 100644 --- a/src/gui/tray/activitydata.h +++ b/src/gui/tray/activitydata.h @@ -17,6 +17,7 @@ #include <QtCore> #include <QIcon> +#include <QJsonObject> namespace OCC { /** @@ -28,13 +29,20 @@ namespace OCC { class ActivityLink { Q_GADGET - + + Q_PROPERTY(QString imageSource MEMBER _imageSource) + Q_PROPERTY(QString imageSourceHovered MEMBER _imageSourceHovered) 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: + static ActivityLink createFomJsonObject(const QJsonObject &obj); + +public: + QString _imageSource; + QString _imageSourceHovered; QString _label; QString _link; QByteArray _verb; @@ -80,11 +88,13 @@ public: QString _message; QString _folder; QString _file; + QString _renamedFile; QUrl _link; QDateTime _dateTime; qint64 _expireAtMsecs = -1; QString _accName; QString _icon; + bool _isCurrentUserFileActivity = false; // Stores information about the error int _status; diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 8120612a5..ef0546f38 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -54,7 +54,7 @@ ActivityListModel::ActivityListModel(AccountState *accountState, QHash<int, QByteArray> ActivityListModel::roleNames() const { - QHash<int, QByteArray> roles; + auto roles = QAbstractListModel::roleNames(); roles[DisplayPathRole] = "displayPath"; roles[PathRole] = "path"; roles[AbsolutePathRole] = "absolutePath"; @@ -65,11 +65,14 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const roles[ActionIconRole] = "icon"; roles[ActionTextRole] = "subject"; roles[ActionsLinksRole] = "links"; + roles[ActionsLinksContextMenuRole] = "linksContextMenu"; + roles[ActionsLinksForActionButtonsRole] = "linksForActionButtons"; roles[ActionTextColorRole] = "activityTextTitleColor"; roles[ObjectTypeRole] = "objectType"; roles[PointInTimeRole] = "dateTime"; roles[DisplayActions] = "displayActions"; roles[ShareableRole] = "isShareable"; + roles[IsCurrentUserFileActivityRole] = "isCurrentUserFileActivity"; return roles; } @@ -78,6 +81,11 @@ void ActivityListModel::setAccountState(AccountState *state) _accountState = state; } +void ActivityListModel::setCurrentItem(const int currentItem) +{ + _currentItem = currentItem; +} + void ActivityListModel::setCurrentlyFetching(bool value) { _currentlyFetching = value; @@ -116,10 +124,11 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const return QVariant(); const auto getFilePath = [&]() { - if (!a._file.isEmpty()) { + const auto fileName = a._fileAction == QStringLiteral("file_renamed") ? a._renamedFile : a._file; + if (!fileName.isEmpty()) { const auto folder = FolderMan::instance()->folder(a._folder); - const QString relPath = folder ? folder->remotePath() + a._file : a._file; + const QString relPath = folder ? folder->remotePath() + fileName : fileName; const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account()); @@ -130,7 +139,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const // If this is an E2EE file or folder, pretend we got no path, hiding the share button which is what we want if (folder) { SyncJournalFileRecord rec; - folder->journalDb()->getFileRecord(a._file.mid(1), &rec); + folder->journalDb()->getFileRecord(fileName.mid(1), &rec); if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) { return QString(); } @@ -169,7 +178,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const case DisplayPathRole: return getDisplayPath(); case PathRole: - return QUrl::fromLocalFile(QFileInfo(getFilePath()).path()); + return QFileInfo(getFilePath()).path(); case AbsolutePathRole: return getFilePath(); case DisplayLocationRole: @@ -181,6 +190,15 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const } return customList; } + + case ActionsLinksContextMenuRole: { + return ActivityListModel::convertLinksToMenuEntries(a); + } + + case ActionsLinksForActionButtonsRole: { + return ActivityListModel::convertLinksToActionButtons(a); + } + case ActionIconRole: { if (a._type == Activity::NotificationType) { return "qrc:///client/theme/black/bell.svg"; @@ -249,7 +267,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const if (a._link.isEmpty()) { return ""; } else { - return a._link; + return a._link.toString(); } } case AccountRole: @@ -262,7 +280,9 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const case DisplayActions: return _displayActions; case ShareableRole: - return !data(index, PathRole).toString().isEmpty() && _displayActions && a._fileAction != "file_deleted" && a._status != SyncFileItem::FileIgnored; + return !data(index, PathRole).toString().isEmpty() && a._objectType == QStringLiteral("files") && _displayActions && a._fileAction != "file_deleted" && a._status != SyncFileItem::FileIgnored; + case IsCurrentUserFileActivityRole: + return a._isCurrentUserFileActivity; default: return QVariant(); } @@ -310,6 +330,21 @@ void ActivityListModel::startFetchJob() job->start(); } +void ActivityListModel::setFinalList(const ActivityList &finalList) +{ + _finalList = finalList; +} + +const ActivityList &ActivityListModel::finalList() const +{ + return _finalList; +} + +int ActivityListModel::currentItem() const +{ + return _currentItem; +} + void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode) { auto activities = json.object().value("ocs").toObject().value("data").toArray(); @@ -333,6 +368,7 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status auto json = activ.toObject(); Activity a; + const auto activityUser = json.value(QStringLiteral("user")).toString(); a._type = Activity::ActivityType; a._objectType = json.value(QStringLiteral("object_type")).toString(); a._accName = ast->account()->displayName(); @@ -344,6 +380,7 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status a._link = QUrl(json.value(QStringLiteral("link")).toString()); a._dateTime = QDateTime::fromString(json.value(QStringLiteral("datetime")).toString(), Qt::ISODate); a._icon = json.value(QStringLiteral("icon")).toString(); + a._isCurrentUserFileActivity = a._objectType == QStringLiteral("files") && activityUser == ast->account()->davUser(); auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray(); @@ -395,9 +432,9 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status _activityLists.append(list); - emit activityJobStatusCode(statusCode); - combineActivityLists(); + + emit activityJobStatusCode(statusCode); } void ActivityListModel::addErrorToActivityList(Activity activity) @@ -486,7 +523,7 @@ void ActivityListModel::removeActivityFromActivityList(Activity activity) } } -void ActivityListModel::triggerDefaultAction(int activityIndex) +void ActivityListModel::slotTriggerDefaultAction(const int activityIndex) { if (activityIndex < 0 || activityIndex >= _finalList.size()) { qCWarning(lcActivity) << "Couldn't trigger default action at index" << activityIndex << "/ final list size:" << _finalList.size(); @@ -494,7 +531,7 @@ void ActivityListModel::triggerDefaultAction(int activityIndex) } const auto modelIndex = index(activityIndex); - const auto path = data(modelIndex, PathRole).toUrl(); + const auto path = data(modelIndex, PathRole).toString(); const auto activity = _finalList.at(activityIndex); if (activity._status == SyncFileItem::Conflict) { @@ -544,15 +581,15 @@ void ActivityListModel::triggerDefaultAction(int activityIndex) return; } - if (path.isValid()) { - QDesktopServices::openUrl(path); + if (!path.isEmpty()) { + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } else { const auto link = data(modelIndex, LinkRole).toUrl(); Utility::openBrowser(link); } } -void ActivityListModel::triggerAction(int activityIndex, int actionIndex) +void ActivityListModel::slotTriggerAction(const int activityIndex, const int actionIndex) { if (activityIndex < 0 || activityIndex >= _finalList.size()) { qCWarning(lcActivity) << "Couldn't trigger action on activity at index" << activityIndex << "/ final list size:" << _finalList.size(); @@ -576,11 +613,112 @@ void ActivityListModel::triggerAction(int activityIndex, int actionIndex) emit sendNotificationRequest(activity._accName, action._link, action._verb, activityIndex); } +void ActivityListModel::slotTriggerDismiss(const int activityIndex) +{ + 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 activityLinks = _finalList[activityIndex]._links; + + const auto foundActivityLinkIt = std::find_if(std::cbegin(activityLinks), std::cend(activityLinks), [](const ActivityLink &link) { + return link._verb == QStringLiteral("DELETE"); + }); + + if (foundActivityLinkIt == std::cend(activityLinks)) { + qCWarning(lcActivity) << "Couldn't find dismiss action in activity at index" << activityIndex + << " links.size() " << activityLinks.size(); + return; + } + + const auto actionIndex = static_cast<int>(std::distance(activityLinks.begin(), foundActivityLinkIt)); + + if (actionIndex < 0 || actionIndex > activityLinks.size()) { + qCWarning(lcActivity) << "Couldn't find dismiss action in activity at index" << activityIndex + << " actionIndex found " << actionIndex; + return; + } + + slotTriggerAction(activityIndex, actionIndex); +} + AccountState *ActivityListModel::accountState() const { return _accountState; } +QVariantList ActivityListModel::convertLinksToActionButtons(const Activity &activity) +{ + QVariantList customList; + + if (activity._links.size() == 1) { + return customList; + } + + if (static_cast<quint32>(activity._links.size()) > maxActionButtons()) { + customList << ActivityListModel::convertLinkToActionButton(activity, activity._links.first()); + return customList; + } + + for (const auto &activityLink : activity._links) { + if (activityLink._verb == QStringLiteral("DELETE") + || (activity._objectType == QStringLiteral("chat") || activity._objectType == QStringLiteral("call") + || activity._objectType == QStringLiteral("room"))) { + customList << ActivityListModel::convertLinkToActionButton(activity, activityLink); + } + } + + return customList; +} + +QVariant ActivityListModel::convertLinkToActionButton(const OCC::Activity &activity, const OCC::ActivityLink &activityLink) +{ + auto activityLinkCopy = activityLink; + + const auto isReplyIconApplicable = activityLink._verb == QStringLiteral("WEB") + && (activity._objectType == QStringLiteral("chat") || activity._objectType == QStringLiteral("call") + || activity._objectType == QStringLiteral("room")); + + const QString replyButtonPath = QStringLiteral("image://svgimage-custom-color/reply.svg"); + + if (isReplyIconApplicable) { + activityLinkCopy._imageSource = + QString(replyButtonPath + "/" + OCC::Theme::instance()->wizardHeaderBackgroundColor().name()); + activityLinkCopy._imageSourceHovered = + QString(replyButtonPath + "/" + OCC::Theme::instance()->wizardHeaderTitleColor().name()); + } + + const auto isReplyLabelApplicable = activityLink._verb == QStringLiteral("WEB") + && (activity._objectType == QStringLiteral("chat") + || (activity._objectType != QStringLiteral("room") && activity._objectType != QStringLiteral("call"))); + + if (activityLink._verb == QStringLiteral("DELETE")) { + activityLinkCopy._label = QObject::tr("Mark as read"); + } else if (isReplyLabelApplicable) { + activityLinkCopy._label = QObject::tr("Reply"); + } + + return QVariant::fromValue(activityLinkCopy); +} + +QVariantList ActivityListModel::convertLinksToMenuEntries(const Activity &activity) +{ + QVariantList customList; + + if (static_cast<quint32>(activity._links.size()) > maxActionButtons()) { + for (int i = 0; i < activity._links.size(); ++i) { + const auto &activityLink = activity._links[i]; + if (!activityLink._primary) { + customList << QVariantMap{ + {QStringLiteral("actionIndex"), i}, {QStringLiteral("label"), activityLink._label}}; + } + } + } + + return customList; +} + void ActivityListModel::combineActivityLists() { ActivityList resultList; diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index 66de2e498..34c591c2f 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -40,6 +40,8 @@ class ActivityListModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(quint32 maxActionButtons READ maxActionButtons CONSTANT) + Q_PROPERTY(AccountState *accountState READ accountState CONSTANT) public: enum DataRole { @@ -47,6 +49,8 @@ public: AccountRole, ObjectTypeRole, ActionsLinksRole, + ActionsLinksContextMenuRole, + ActionsLinksForActionButtonsRole, ActionTextRole, ActionTextColorRole, ActionRole, @@ -60,6 +64,7 @@ public: AccountConnectedRole, DisplayActions, ShareableRole, + IsCurrentUserFileActivityRole, }; Q_ENUM(DataRole) @@ -84,15 +89,22 @@ public: void removeActivityFromActivityList(int row); void removeActivityFromActivityList(Activity activity); - Q_INVOKABLE void triggerDefaultAction(int activityIndex); - Q_INVOKABLE void triggerAction(int activityIndex, int actionIndex); - AccountState *accountState() const; void setAccountState(AccountState *state); + static constexpr quint32 maxActionButtons() + { + return MaxActionButtons; + } + + void setCurrentItem(const int currentItem); + public slots: void slotRefreshActivity(); void slotRemoveAccount(); + void slotTriggerDefaultAction(const int activityIndex); + void slotTriggerAction(const int activityIndex, const int actionIndex); + void slotTriggerDismiss(const int activityIndex); signals: void activityJobStatusCode(int statusCode); @@ -110,7 +122,16 @@ protected: virtual void startFetchJob(); + // added these for unit tests + void setFinalList(const ActivityList &finalList); + const ActivityList &finalList() const; + int currentItem() const; + // + private: + static QVariantList convertLinksToMenuEntries(const Activity &activity); + static QVariantList convertLinksToActionButtons(const Activity &activity); + static QVariant convertLinkToActionButton(const Activity &activity, const ActivityLink &activityLink); void combineActivityLists(); bool canFetchActivities() const; @@ -137,6 +158,8 @@ private: bool _currentlyFetching = false; bool _doneFetching = false; bool _hideOldActivities = true; + + static constexpr quint32 MaxActionButtons = 2; }; } diff --git a/src/gui/tray/notificationhandler.cpp b/src/gui/tray/notificationhandler.cpp index ccac7b1de..9acd325b9 100644 --- a/src/gui/tray/notificationhandler.cpp +++ b/src/gui/tray/notificationhandler.cpp @@ -121,14 +121,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j auto actions = json.value("actions").toArray(); foreach (auto action, actions) { - auto actionJson = action.toObject(); - ActivityLink al; - al._label = QUrl::fromPercentEncoding(actionJson.value("label").toString().toUtf8()); - al._link = actionJson.value("link").toString(); - al._verb = actionJson.value("type").toString().toUtf8(); - al._primary = actionJson.value("primary").toBool(); - - a._links.append(al); + a._links.append(ActivityLink::createFomJsonObject(action.toObject())); } // Add another action to dismiss notification on server diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp index a3200df3b..44e9f1a0a 100644 --- a/src/gui/tray/usermodel.cpp +++ b/src/gui/tray/usermodel.cpp @@ -514,6 +514,7 @@ void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr activity._fileAction = "file_created"; } else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) { activity._fileAction = "file_renamed"; + activity._renamedFile = item->_renameTarget; } else { activity._fileAction = "file_changed"; } |