diff --git a/TODO.md b/TODO.md
index bf402a2b..640ca9c1 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,5 +1,11 @@
# TODO
+- precise text selection for one message
+- long-press-drag to select multiple messages on touch
+- drag to select on non-touch
+- shift+click to select everything in between
+
+- remove radius on invite/left banner
- side pane back/forward buttons hard to use on touch
- is it still slow on sway with wayland-egl?
- room pane drag-scroll a tiny bit activates the delegates
diff --git a/src/gui/Base/HImage.qml b/src/gui/Base/HImage.qml
index aac80469..6895d4bf 100644
--- a/src/gui/Base/HImage.qml
+++ b/src/gui/Base/HImage.qml
@@ -21,6 +21,7 @@ Image {
property bool animate: true
property bool animated:
utils.urlExtension(image.source).toLowerCase() === "gif"
+ property bool enabledAnimatedPausing: true
property alias radius: roundMask.radius
property alias showProgressBar: progressBarLoader.active
@@ -74,7 +75,9 @@ Image {
property bool userPaused: ! window.settings.media.autoPlayGIF
TapHandler {
+ enabled: image.enabledAnimatedPausing
onTapped: parent.userPaused = ! parent.userPaused
+ gesturePolicy: TapHandler.ReleaseWithinBounds
}
HIcon {
diff --git a/src/gui/Base/HListView.qml b/src/gui/Base/HListView.qml
index 0f0475d7..a8699156 100644
--- a/src/gui/Base/HListView.qml
+++ b/src/gui/Base/HListView.qml
@@ -68,6 +68,31 @@ ListView {
property bool allowDragging: true
property alias cursorShape: mouseArea.cursorShape
property int currentItemHeight: currentItem ? currentItem.height : 0
+ property var checkedDelegates: ({})
+ property int selectedCount: Object.keys(checkedDelegates).length
+
+
+ function delegatesChecked(...indices) {
+ for (const i of indices) {
+ const model = listView.model.get(i)
+ checkedDelegates[model.id] = model
+ }
+ checkedDelegatesChanged()
+ }
+
+ function delegatesUnchecked(...indices) {
+ for (const i of indices) {
+ const model = listView.model.get(i)
+ delete checkedDelegates[model.id]
+ }
+ checkedDelegatesChanged()
+ }
+
+ function getSortedCheckedDelegates() {
+ return Object.values(checkedDelegates).sort(
+ (a, b) => a.date > b.date ? 1 : -1
+ )
+ }
Connections {
diff --git a/src/gui/Base/HSelectableLabel.qml b/src/gui/Base/HSelectableLabel.qml
index 5df02ac6..f3aeb110 100644
--- a/src/gui/Base/HSelectableLabel.qml
+++ b/src/gui/Base/HSelectableLabel.qml
@@ -13,11 +13,13 @@ TextEdit {
tabStopDistance: 4 * 4 // 4 spaces
readOnly: true
- persistentSelection: true
activeFocusOnPress: false
focus: false
- onLinkActivated: Qt.openUrlExternally(link)
+ onLinkActivated: if (enableLinkActivation) Qt.openUrlExternally(link)
+
+
+ property bool enableLinkActivation: true
function selectWordAt(position) {
diff --git a/src/gui/Base/HTile.qml b/src/gui/Base/HTile.qml
index 677498f6..2efaf9b9 100644
--- a/src/gui/Base/HTile.qml
+++ b/src/gui/Base/HTile.qml
@@ -9,6 +9,7 @@ HButton {
signal leftClicked()
signal rightClicked()
+ signal longPressed()
default property alias additionalData: contentItem.data
@@ -106,6 +107,7 @@ HButton {
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: leftClicked()
+ onLongPressed: tile.longPressed()
}
TapHandler {
diff --git a/src/gui/Pages/Chat/Timeline/EventContent.qml b/src/gui/Pages/Chat/Timeline/EventContent.qml
index 9060b3b7..ee892c71 100644
--- a/src/gui/Pages/Chat/Timeline/EventContent.qml
+++ b/src/gui/Pages/Chat/Timeline/EventContent.qml
@@ -110,6 +110,9 @@ HRowLayout {
HSelectableLabel {
id: contentLabel
visible: ! pureMedia
+ enableLinkActivation: ! eventList.selectedCount
+
+ // selectByMouse: eventDelegate.checked XXX
topPadding: theme.chat.message.verticalSpacing
bottomPadding: topPadding
@@ -165,6 +168,13 @@ HRowLayout {
HoverHandler { id: contentHover }
+ TapHandler {
+ acceptedButtons: Qt.LeftButton
+ onTapped:
+ if (! parent.hoveredLink || ! parent.enableLinkActivation)
+ eventDelegate.toggleChecked()
+ }
+
Rectangle {
width: Math.max(
parent.paintedWidth +
@@ -176,7 +186,9 @@ HRowLayout {
height: contentColumn.height
radius: theme.chat.message.radius
z: -100
- color: isOwn?
+ color: eventDelegate.checked ?
+ "blue" : // XXX
+ isOwn?
theme.chat.message.ownBackground :
theme.chat.message.background
diff --git a/src/gui/Pages/Chat/Timeline/EventDelegate.qml b/src/gui/Pages/Chat/Timeline/EventDelegate.qml
index 7def3fdd..4cc57781 100644
--- a/src/gui/Pages/Chat/Timeline/EventDelegate.qml
+++ b/src/gui/Pages/Chat/Timeline/EventDelegate.qml
@@ -10,6 +10,8 @@ HColumnLayout {
id: eventDelegate
width: eventList.width
+ ListView.onRemove: eventList.delegatesUnchecked(model.id)
+
enum Media { Page, File, Image, Video, Audio }
@@ -20,6 +22,7 @@ HColumnLayout {
readonly property var nextModel: eventList.model.get(model.index - 1)
readonly property QtObject currentModel: model
+ property bool checked: model.id in eventList.checkedDelegates
property bool compact: window.settings.compactMode
property bool isOwn: chat.userId === model.sender_id
property bool onRight: eventList.ownEventsOnRight && isOwn
@@ -73,9 +76,14 @@ HColumnLayout {
contextMenu.popup()
}
+ function toggleChecked() {
+ eventDelegate.checked ?
+ eventList.delegatesUnchecked(model.index) :
+ eventList.delegatesChecked(model.index)
+ }
+
Item {
-
Layout.fillWidth: true
Layout.preferredHeight:
model.event_type === "RoomCreateEvent" ? 0 : separationSpacing
@@ -103,6 +111,10 @@ HColumnLayout {
Behavior on x { HNumberAnimation {} }
}
+ TapHandler {
+ acceptedButtons: Qt.LeftButton
+ onTapped: toggleChecked()
+ }
TapHandler {
acceptedButtons: Qt.RightButton
@@ -123,6 +135,19 @@ HColumnLayout {
onClosed: { media = []; link = "" }
+ HMenuItem {
+ icon.name: "toggle-select-message"
+ text: eventDelegate.checked ? qsTr("Unselect") : qsTr("Select")
+ onTriggered: eventDelegate.toggleChecked()
+ }
+
+ HMenuItem {
+ visible: eventList.selectedCount >= 2
+ icon.name: "unselect-all-messages"
+ text: qsTr("Unselect all")
+ onTriggered: eventList.checkedDelegates = {}
+ }
+
HMenuItem {
id: copyMedia
icon.name: "copy-link"
@@ -161,10 +186,21 @@ HColumnLayout {
HMenuItem {
icon.name: "copy-text"
text: qsTr("Copy text")
- visible: enabled || (! copyLink.visible && ! copyMedia.visible)
- enabled: Boolean(selectableLabelContainer.joinedSelection)
- onTriggered:
- Clipboard.text = selectableLabelContainer.joinedSelection
+ visible: ! copyLink.visible && ! copyMedia.visible
+ onTriggered: {
+ if (! eventList.selectedCount) {
+ Clipboard.text = JSON.parse(model.source).body
+ return
+ }
+
+ const contents = []
+
+ for (const model of eventList.getSortedCheckedDelegates()) {
+ contents.push(JSON.parse(model.source).body)
+ }
+
+ Clipboard.text = contents.join("\n\n")
+ }
}
HMenuItem {
diff --git a/src/gui/Pages/Chat/Timeline/EventFile.qml b/src/gui/Pages/Chat/Timeline/EventFile.qml
index b45eeffe..cda0f42b 100644
--- a/src/gui/Pages/Chat/Timeline/EventFile.qml
+++ b/src/gui/Pages/Chat/Timeline/EventFile.qml
@@ -22,8 +22,10 @@ HTile {
svgName: "download"
}
- onLeftClicked: download(Qt.openUrlExternally)
onRightClicked: eventDelegate.openContextMenu()
+ onLeftClicked:
+ eventList.selectedCount ?
+ eventDelegate.toggleChecked() : download(Qt.openUrlExternally)
onHoveredChanged: {
if (! hovered) {
@@ -44,4 +46,10 @@ HTile {
JSON.parse(loader.singleMediaInfo.media_crypt_dict)
readonly property bool isEncrypted: ! utils.isEmptyObject(cryptDict)
+
+
+ Binding on backgroundColor {
+ value: "blue" // XXX
+ when: eventDelegate.checked
+ }
}
diff --git a/src/gui/Pages/Chat/Timeline/EventImage.qml b/src/gui/Pages/Chat/Timeline/EventImage.qml
index b220d49a..2c13119b 100644
--- a/src/gui/Pages/Chat/Timeline/EventImage.qml
+++ b/src/gui/Pages/Chat/Timeline/EventImage.qml
@@ -8,6 +8,7 @@ HMxcImage {
width: fitSize.width
height: fitSize.height
horizontalAlignment: Image.AlignLeft
+ enabledAnimatedPausing: ! eventList.selectedCount
title: thumbnail ? loader.thumbnailTitle : loader.title
animated: loader.singleMediaInfo.media_mime === "image/gif" ||
@@ -82,8 +83,11 @@ HMxcImage {
TapHandler {
- onTapped: if (! image.animated) getOpenUrl(Qt.openUrlExternally)
- onDoubleTapped: getOpenUrl(Qt.openUrlExternally)
+ onTapped:
+ eventList.selectedCount ?
+ eventDelegate.toggleChecked() : getOpenUrl(Qt.openUrlExternally)
+
+ gesturePolicy: TapHandler.ReleaseWithinBounds
}
HoverHandler {
@@ -123,4 +127,12 @@ HMxcImage {
Behavior on opacity { HNumberAnimation {} }
}
+
+ Rectangle {
+ anchors.fill: parent
+ visible: eventDelegate.checked
+ // XXX
+ color: "blue"
+ opacity: 0.2
+ }
}
diff --git a/src/gui/Pages/Chat/Timeline/EventList.qml b/src/gui/Pages/Chat/Timeline/EventList.qml
index 7d69dd20..56b5d4a0 100644
--- a/src/gui/Pages/Chat/Timeline/EventList.qml
+++ b/src/gui/Pages/Chat/Timeline/EventList.qml
@@ -10,10 +10,14 @@ Rectangle {
color: theme.chat.eventList.background
- property Item selectableLabelContainer: Item {}
property alias eventList: eventList
+ HShortcut {
+ sequence: "Escape"
+ onActivated: eventList.checkedDelegates = {}
+ }
+
HListView {
id: eventList
clip: true
@@ -136,6 +140,7 @@ Rectangle {
}
}
+
HNoticePage {
text: qsTr("No messages to show yet")
@@ -150,7 +155,7 @@ Rectangle {
const left = centroid.pressedButtons & Qt.LeftButton
const vel = centroid.velocity.y
const pos = centroid.position.y
- const dist = Math.min(selectableLabelContainer.height / 4, 50)
+ const dist = Math.min(eventList.height / 4, 50)
const boost = 20 * (pos < dist ? -pos : -(height - pos))
dragFlicker.speed =
diff --git a/src/icons/thin/toggle-select-message.svg b/src/icons/thin/toggle-select-message.svg
new file mode 100644
index 00000000..0ec9bc77
--- /dev/null
+++ b/src/icons/thin/toggle-select-message.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/unselect-all-messages.svg b/src/icons/thin/unselect-all-messages.svg
new file mode 100644
index 00000000..5e6a322f
--- /dev/null
+++ b/src/icons/thin/unselect-all-messages.svg
@@ -0,0 +1,3 @@
+