Start implementing new message selection system

This commit is contained in:
miruka 2020-03-25 23:06:51 -04:00
parent 710dba09ec
commit 3852357614
12 changed files with 130 additions and 13 deletions

View File

@ -1,5 +1,11 @@
# TODO # 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 - side pane back/forward buttons hard to use on touch
- is it still slow on sway with wayland-egl? - is it still slow on sway with wayland-egl?
- room pane drag-scroll a tiny bit activates the delegates - room pane drag-scroll a tiny bit activates the delegates

View File

@ -21,6 +21,7 @@ Image {
property bool animate: true property bool animate: true
property bool animated: property bool animated:
utils.urlExtension(image.source).toLowerCase() === "gif" utils.urlExtension(image.source).toLowerCase() === "gif"
property bool enabledAnimatedPausing: true
property alias radius: roundMask.radius property alias radius: roundMask.radius
property alias showProgressBar: progressBarLoader.active property alias showProgressBar: progressBarLoader.active
@ -74,7 +75,9 @@ Image {
property bool userPaused: ! window.settings.media.autoPlayGIF property bool userPaused: ! window.settings.media.autoPlayGIF
TapHandler { TapHandler {
enabled: image.enabledAnimatedPausing
onTapped: parent.userPaused = ! parent.userPaused onTapped: parent.userPaused = ! parent.userPaused
gesturePolicy: TapHandler.ReleaseWithinBounds
} }
HIcon { HIcon {

View File

@ -68,6 +68,31 @@ ListView {
property bool allowDragging: true property bool allowDragging: true
property alias cursorShape: mouseArea.cursorShape property alias cursorShape: mouseArea.cursorShape
property int currentItemHeight: currentItem ? currentItem.height : 0 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 { Connections {

View File

@ -13,11 +13,13 @@ TextEdit {
tabStopDistance: 4 * 4 // 4 spaces tabStopDistance: 4 * 4 // 4 spaces
readOnly: true readOnly: true
persistentSelection: true
activeFocusOnPress: false activeFocusOnPress: false
focus: false focus: false
onLinkActivated: Qt.openUrlExternally(link) onLinkActivated: if (enableLinkActivation) Qt.openUrlExternally(link)
property bool enableLinkActivation: true
function selectWordAt(position) { function selectWordAt(position) {

View File

@ -9,6 +9,7 @@ HButton {
signal leftClicked() signal leftClicked()
signal rightClicked() signal rightClicked()
signal longPressed()
default property alias additionalData: contentItem.data default property alias additionalData: contentItem.data
@ -106,6 +107,7 @@ HButton {
TapHandler { TapHandler {
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
onTapped: leftClicked() onTapped: leftClicked()
onLongPressed: tile.longPressed()
} }
TapHandler { TapHandler {

View File

@ -110,6 +110,9 @@ HRowLayout {
HSelectableLabel { HSelectableLabel {
id: contentLabel id: contentLabel
visible: ! pureMedia visible: ! pureMedia
enableLinkActivation: ! eventList.selectedCount
// selectByMouse: eventDelegate.checked XXX
topPadding: theme.chat.message.verticalSpacing topPadding: theme.chat.message.verticalSpacing
bottomPadding: topPadding bottomPadding: topPadding
@ -165,6 +168,13 @@ HRowLayout {
HoverHandler { id: contentHover } HoverHandler { id: contentHover }
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped:
if (! parent.hoveredLink || ! parent.enableLinkActivation)
eventDelegate.toggleChecked()
}
Rectangle { Rectangle {
width: Math.max( width: Math.max(
parent.paintedWidth + parent.paintedWidth +
@ -176,7 +186,9 @@ HRowLayout {
height: contentColumn.height height: contentColumn.height
radius: theme.chat.message.radius radius: theme.chat.message.radius
z: -100 z: -100
color: isOwn? color: eventDelegate.checked ?
"blue" : // XXX
isOwn?
theme.chat.message.ownBackground : theme.chat.message.ownBackground :
theme.chat.message.background theme.chat.message.background

View File

@ -10,6 +10,8 @@ HColumnLayout {
id: eventDelegate id: eventDelegate
width: eventList.width width: eventList.width
ListView.onRemove: eventList.delegatesUnchecked(model.id)
enum Media { Page, File, Image, Video, Audio } enum Media { Page, File, Image, Video, Audio }
@ -20,6 +22,7 @@ HColumnLayout {
readonly property var nextModel: eventList.model.get(model.index - 1) readonly property var nextModel: eventList.model.get(model.index - 1)
readonly property QtObject currentModel: model readonly property QtObject currentModel: model
property bool checked: model.id in eventList.checkedDelegates
property bool compact: window.settings.compactMode property bool compact: window.settings.compactMode
property bool isOwn: chat.userId === model.sender_id property bool isOwn: chat.userId === model.sender_id
property bool onRight: eventList.ownEventsOnRight && isOwn property bool onRight: eventList.ownEventsOnRight && isOwn
@ -73,9 +76,14 @@ HColumnLayout {
contextMenu.popup() contextMenu.popup()
} }
function toggleChecked() {
eventDelegate.checked ?
eventList.delegatesUnchecked(model.index) :
eventList.delegatesChecked(model.index)
}
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Layout.preferredHeight:
model.event_type === "RoomCreateEvent" ? 0 : separationSpacing model.event_type === "RoomCreateEvent" ? 0 : separationSpacing
@ -103,6 +111,10 @@ HColumnLayout {
Behavior on x { HNumberAnimation {} } Behavior on x { HNumberAnimation {} }
} }
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: toggleChecked()
}
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
@ -123,6 +135,19 @@ HColumnLayout {
onClosed: { media = []; link = "" } 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 { HMenuItem {
id: copyMedia id: copyMedia
icon.name: "copy-link" icon.name: "copy-link"
@ -161,10 +186,21 @@ HColumnLayout {
HMenuItem { HMenuItem {
icon.name: "copy-text" icon.name: "copy-text"
text: qsTr("Copy text") text: qsTr("Copy text")
visible: enabled || (! copyLink.visible && ! copyMedia.visible) visible: ! copyLink.visible && ! copyMedia.visible
enabled: Boolean(selectableLabelContainer.joinedSelection) onTriggered: {
onTriggered: if (! eventList.selectedCount) {
Clipboard.text = selectableLabelContainer.joinedSelection 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 { HMenuItem {

View File

@ -22,8 +22,10 @@ HTile {
svgName: "download" svgName: "download"
} }
onLeftClicked: download(Qt.openUrlExternally)
onRightClicked: eventDelegate.openContextMenu() onRightClicked: eventDelegate.openContextMenu()
onLeftClicked:
eventList.selectedCount ?
eventDelegate.toggleChecked() : download(Qt.openUrlExternally)
onHoveredChanged: { onHoveredChanged: {
if (! hovered) { if (! hovered) {
@ -44,4 +46,10 @@ HTile {
JSON.parse(loader.singleMediaInfo.media_crypt_dict) JSON.parse(loader.singleMediaInfo.media_crypt_dict)
readonly property bool isEncrypted: ! utils.isEmptyObject(cryptDict) readonly property bool isEncrypted: ! utils.isEmptyObject(cryptDict)
Binding on backgroundColor {
value: "blue" // XXX
when: eventDelegate.checked
}
} }

View File

@ -8,6 +8,7 @@ HMxcImage {
width: fitSize.width width: fitSize.width
height: fitSize.height height: fitSize.height
horizontalAlignment: Image.AlignLeft horizontalAlignment: Image.AlignLeft
enabledAnimatedPausing: ! eventList.selectedCount
title: thumbnail ? loader.thumbnailTitle : loader.title title: thumbnail ? loader.thumbnailTitle : loader.title
animated: loader.singleMediaInfo.media_mime === "image/gif" || animated: loader.singleMediaInfo.media_mime === "image/gif" ||
@ -82,8 +83,11 @@ HMxcImage {
TapHandler { TapHandler {
onTapped: if (! image.animated) getOpenUrl(Qt.openUrlExternally) onTapped:
onDoubleTapped: getOpenUrl(Qt.openUrlExternally) eventList.selectedCount ?
eventDelegate.toggleChecked() : getOpenUrl(Qt.openUrlExternally)
gesturePolicy: TapHandler.ReleaseWithinBounds
} }
HoverHandler { HoverHandler {
@ -123,4 +127,12 @@ HMxcImage {
Behavior on opacity { HNumberAnimation {} } Behavior on opacity { HNumberAnimation {} }
} }
Rectangle {
anchors.fill: parent
visible: eventDelegate.checked
// XXX
color: "blue"
opacity: 0.2
}
} }

View File

@ -10,10 +10,14 @@ Rectangle {
color: theme.chat.eventList.background color: theme.chat.eventList.background
property Item selectableLabelContainer: Item {}
property alias eventList: eventList property alias eventList: eventList
HShortcut {
sequence: "Escape"
onActivated: eventList.checkedDelegates = {}
}
HListView { HListView {
id: eventList id: eventList
clip: true clip: true
@ -136,6 +140,7 @@ Rectangle {
} }
} }
HNoticePage { HNoticePage {
text: qsTr("No messages to show yet") text: qsTr("No messages to show yet")
@ -150,7 +155,7 @@ Rectangle {
const left = centroid.pressedButtons & Qt.LeftButton const left = centroid.pressedButtons & Qt.LeftButton
const vel = centroid.velocity.y const vel = centroid.velocity.y
const pos = centroid.position.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)) const boost = 20 * (pos < dist ? -pos : -(height - pos))
dragFlicker.speed = dragFlicker.speed =

View File

@ -0,0 +1,3 @@
<svg clip-rule="evenodd" fill-rule="evenodd" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m24 6.278-11.16 12.722-6.84-6 1.319-1.49 5.341 4.686 9.865-11.196zm-22.681 5.232 6.835 6.01-1.314 1.48-6.84-6zm9.278.218 5.921-6.728 1.482 1.285-5.921 6.756z"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m11 24v-2h-4v2zm8-22h3v3h2v-5h-5zm-19 15h2v-4h-2zm0-6h2v-4h-2zm2-6v-3h3v-2h-5v5zm22 2h-2v4h2zm0 6h-2v4h2zm-2 6v3h-3v2h5v-5zm-17 3h-3v-3h-2v5h5zm12 2v-2h-4v2zm-6-22v-2h-4v2zm6 0v-2h-4v2zm0 11h-10v-2h10z"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B