Start implementing new message selection system
This commit is contained in:
parent
710dba09ec
commit
3852357614
6
TODO.md
6
TODO.md
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
3
src/icons/thin/toggle-select-message.svg
Normal file
3
src/icons/thin/toggle-select-message.svg
Normal 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 |
3
src/icons/thin/unselect-all-messages.svg
Normal file
3
src/icons/thin/unselect-all-messages.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user