Remove message text selection hack

This commit is contained in:
miruka 2020-03-24 11:26:17 -04:00
parent f148837fae
commit 710dba09ec
6 changed files with 174 additions and 366 deletions

View File

@ -1,5 +1,9 @@
# TODO
- 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
## Goals before 0.5.0
- Redacting messages
@ -13,8 +17,6 @@
## Refactoring
- Rewrite the message text selection buggy mess
- Put keybindings in the components they belong to instead of shoving them
all in one central file

View File

@ -19,120 +19,26 @@ TextEdit {
onLinkActivated: Qt.openUrlExternally(link)
Component.onCompleted: updateSelection()
// If index is a whole number, the label will get two \n before itself
// in container.joinedSelection. If it's a decimal number, if gets one \n.
property real index
property HSelectableLabelContainer container
property bool selectable: true
function updateSelection() {
if (! selectable && label.selectedText) {
label.deselect()
updateContainerSelectedTexts()
return
}
if (! selectable) return
if (! container.reversed &&
container.selectionStart <= container.selectionEnd ||
container.reversed &&
container.selectionStart > container.selectionEnd)
{
var first = container.selectionStart
var firstPos = container.selectionStartPosition
var last = container.selectionEnd
var lastPos = container.selectionEndPosition
} else {
var first = container.selectionEnd
var firstPos = container.selectionEndPosition
var last = container.selectionStart
var lastPos = container.selectionStartPosition
}
if (first === index && last === index) {
select(
label.positionAt(firstPos.x, firstPos.y),
label.positionAt(lastPos.x, lastPos.y),
)
} else if ((! container.reversed && first < index && index < last) ||
(container.reversed && first > index && index > last))
{
label.selectAll()
} else if (first === index) {
label.select(positionAt(firstPos.x, firstPos.y), length)
} else if (last === index) {
label.select(0, positionAt(lastPos.x, lastPos.y))
} else {
label.deselect()
}
updateContainerSelectedTexts()
}
function updateContainerSelectedTexts() {
container.selectedTexts[index] = selectedText
container.selectedTextsChanged()
}
function selectWordAt(position) {
container.clearSelection()
label.cursorPosition = positionAt(position.x, position.y)
label.selectWord()
updateContainerSelectedTexts()
}
function selectAllText() {
container.clearSelection()
label.selectAll()
updateContainerSelectedTexts()
}
Connections {
target: container
onSelectionInfoChanged: updateSelection()
onDeselectAll: deselect()
}
DropArea {
anchors.fill: parent
onPositionChanged: {
if (! container.selecting) {
container.clearSelection()
container.selectionStart = index
container.selectionStartPosition = Qt.point(drag.x, drag.y)
container.selecting = true
} else {
container.selectionEnd = index
container.selectionEndPosition = Qt.point(drag.x, drag.y)
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: {
tapCount === 2 ? selectWordAt(eventPoint.position) :
tapCount === 3 ? selectAllText() :
container.clearSelection()
}
}
PointHandler {
onActiveChanged:
active ? container.dragStarted() : container.dragStopped()
onPointChanged: container.dragPointChanged(point)
}
// XXX
// TapHandler {
// acceptedButtons: Qt.LeftButton
// onTapped: {
// tapCount === 2 ? selectWordAt(eventPoint.position) :
// tapCount === 3 ? selectAllText() :
// null
// }
// }
MouseArea {
anchors.fill: label

View File

@ -1,92 +0,0 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import Clipboard 0.1
FocusScope {
signal deselectAll()
signal dragStarted()
signal dragStopped()
signal dragPointChanged(var eventPoint)
property bool reversed: false
property bool selecting: false
property real selectionStart: -1
property real selectionEnd: -1
property point selectionStartPosition: Qt.point(-1, -1)
property point selectionEndPosition: Qt.point(-1, -1)
property var selectedTexts: ({})
readonly property var selectionInfo: [
selectionStart, selectionStartPosition,
selectionEnd, selectionEndPosition,
]
readonly property string joinedSelection: {
const toCopy = []
for (const key of Object.keys(selectedTexts).sort()) {
if (! selectedTexts[key]) continue
// For some dumb reason, Object.keys convert the floats to strings
toCopy.push(Number.isInteger(parseFloat(key)) ? "\n\n" : "\n")
toCopy.push(selectedTexts[key])
}
if (reversed) toCopy.reverse()
return toCopy.join("").trim()
}
onJoinedSelectionChanged:
if (joinedSelection) Clipboard.selection = joinedSelection
onDragStarted: {
draggedItem.Drag.active = true
}
onDragStopped: {
draggedItem.Drag.drop()
draggedItem.Drag.active = false
selecting = false
}
onDragPointChanged: {
const pos = mapFromItem(
mainUI, eventPoint.scenePosition.x, eventPoint.scenePosition.y,
)
draggedItem.x = pos.x
draggedItem.y = pos.y
}
function clearSelection() {
selecting = false
selectionStart = -1
selectionEnd = -1
selectionStartPosition = Qt.point(-1, -1)
selectionEndPosition = Qt.point(-1, -1)
deselectAll()
}
// PointHandler and TapHandler won't activate if the press occurs inside
// a label child, so we need a Point/TapHandler inside them too.
PointHandler {
// We don't use a DragHandler because they have an unchangable minimum
// drag distance before they activate.
id: pointHandler
onActiveChanged: active ? dragStarted() : dragStopped()
onPointChanged: dragPointChanged(point)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: clearSelection()
}
// This item will trigger the children labels's DropAreas
Item { id: draggedItem }
}

View File

@ -165,9 +165,10 @@ Rectangle {
}
}
area.onSelectedTextChanged: if (area.selectedText && eventList) {
eventList.selectableLabelContainer.clearSelection()
}
// XXX
// area.onSelectedTextChanged: if (area.selectedText && eventList) {
// eventList.selectableLabelContainer.clearSelection()
// }
Component.onCompleted: {
area.Keys.onReturnPressed.connect(ev => {
@ -206,15 +207,16 @@ Rectangle {
})
area.Keys.onPressed.connect(ev => {
if (ev.matches(StandardKey.Copy) &&
eventList &&
eventList.selectableLabelContainer.joinedSelection
) {
ev.accepted = true
Clipboard.text =
eventList.selectableLabelContainer.joinedSelection
return
}
// XXX
// if (ev.matches(StandardKey.Copy) &&
// eventList &&
// eventList.selectableLabelContainer.joinedSelection
// ) {
// ev.accepted = true
// Clipboard.text =
// eventList.selectableLabelContainer.joinedSelection
// return
// }
// FIXME: buggy
// if (ev.modifiers === Qt.NoModifier &&

View File

@ -109,8 +109,6 @@ HRowLayout {
HSelectableLabel {
id: contentLabel
container: selectableLabelContainer
index: model.index
visible: ! pureMedia
topPadding: theme.chat.message.verticalSpacing
@ -158,13 +156,11 @@ HRowLayout {
function selectAllText() {
// Select the message body without the date or name
container.clearSelection()
contentLabel.select(
0,
contentLabel.length -
timeText.length - 1 // - 1: separating space
)
contentLabel.updateContainerSelectedTexts()
}
HoverHandler { id: contentHover }

View File

@ -7,183 +7,132 @@ import "../../.."
import "../../../Base"
Rectangle {
property alias selectableLabelContainer: selectableLabelContainer
property alias eventList: eventList
color: theme.chat.eventList.background
HSelectableLabelContainer {
id: selectableLabelContainer
property Item selectableLabelContainer: Item {}
property alias eventList: eventList
HListView {
id: eventList
clip: true
anchors.fill: parent
reversed: eventList.verticalLayoutDirection === ListView.BottomToTop
anchors.leftMargin: theme.spacing
anchors.rightMargin: theme.spacing
DragHandler {
target: null
onActiveChanged: if (! active) dragFlicker.speed = 0
onCentroidChanged: {
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 boost = 20 * (pos < dist ? -pos : -(height - pos))
topMargin: theme.spacing
bottomMargin: theme.spacing
verticalLayoutDirection: ListView.BottomToTop
dragFlicker.speed =
left && vel && pos < dist ? 1000 + boost :
left && vel && pos > height - dist ? -1000 + -boost :
0
// Keep x scroll pages cached, to limit images having to be
// reloaded from network.
cacheBuffer: Screen.desktopAvailableHeight * 2
model: ModelStore.get(chat.userId, chat.roomId, "events")
delegate: EventDelegate {}
// Since the list is BottomToTop, this is actually a header
footer: Item {
width: eventList.width
height: (button.height + theme.spacing * 2) * opacity
opacity: eventList.loading ? 1 : 0
visible: opacity > 0
Behavior on opacity { HNumberAnimation {} }
HButton {
id: button
width: Math.min(parent.width, implicitWidth)
anchors.centerIn: parent
loading: true
text: qsTr("Loading previous messages...")
enableRadius: true
iconItem.small: true
}
}
Timer {
id: dragFlicker
interval: 100
running: speed !== 0
repeat: true
onYPosChanged:
if (canLoad && yPos < 0.1) Qt.callLater(loadPastEvents)
onTriggered: {
if (eventList.verticalOvershoot !== 0) return
if (speed < 0 && eventList.atYEnd) return
if (eventList.atYBeggining) {
if (bouncedStart) { return } else { bouncedStart = true }
}
// When an invited room becomes joined, we should now be able to
// fetch past events.
onInviterChanged: canLoad = true
eventList.flick(0, speed * acceleration)
acceleration = Math.min(8, acceleration * 1.05)
}
onRunningChanged: if (! running) {
acceleration = 1.0
bouncedStart = false
eventList.cancelFlick()
eventList.returnToBounds()
}
Component.onCompleted: shortcuts.flickTarget = eventList
property real speed: 0.0
property real acceleration: 1.0
property bool bouncedStart: false
property string inviter: chat.roomInfo.inviter || ""
property real yPos: visibleArea.yPosition
property bool canLoad: true
property bool loading: false
property bool ownEventsOnRight:
width < theme.chat.eventList.ownEventsOnRightUnderWidth
function canCombine(item, itemAfter) {
if (! item || ! itemAfter) return false
return Boolean(
! canTalkBreak(item, itemAfter) &&
! canDayBreak(item, itemAfter) &&
item.sender_id === itemAfter.sender_id &&
utils.minutesBetween(item.date, itemAfter.date) <= 5
)
}
HListView {
id: eventList
clip: true
allowDragging: false
function canTalkBreak(item, itemAfter) {
if (! item || ! itemAfter) return false
anchors.fill: parent
anchors.leftMargin: theme.spacing
anchors.rightMargin: theme.spacing
return Boolean(
! canDayBreak(item, itemAfter) &&
utils.minutesBetween(item.date, itemAfter.date) >= 20
)
}
topMargin: theme.spacing
bottomMargin: theme.spacing
verticalLayoutDirection: ListView.BottomToTop
function canDayBreak(item, itemAfter) {
if (itemAfter && itemAfter.event_type === "RoomCreateEvent")
return true
// Keep x scroll pages cached, to limit images having to be
// reloaded from network.
cacheBuffer: Screen.desktopAvailableHeight * 2
if (! item || ! itemAfter || ! item.date || ! itemAfter.date)
return false
onYPosChanged:
if (canLoad && yPos < 0.1) Qt.callLater(loadPastEvents)
return item.date.getDate() !== itemAfter.date.getDate()
}
// When an invited room becomes joined, we should now be able to
// fetch past events.
onInviterChanged: canLoad = true
function loadPastEvents() {
// try/catch blocks to hide pyotherside error when the
// component is destroyed but func is still running
// Since the list is BottomToTop, this is actually a header
footer: Item {
width: eventList.width
height: (button.height + theme.spacing * 2) * opacity
opacity: eventList.loading ? 1 : 0
visible: opacity > 0
try {
eventList.canLoad = false
eventList.loading = true
Behavior on opacity { HNumberAnimation {} }
py.callClientCoro(
chat.userId,
"load_past_events",
[chat.roomId],
moreToLoad => {
try {
eventList.canLoad = moreToLoad
HButton {
id: button
width: Math.min(parent.width, implicitWidth)
anchors.centerIn: parent
// Call yPosChanged() to run this func again
// if the loaded messages aren't enough to fill
// the screen.
if (moreToLoad) yPosChanged()
loading: true
text: qsTr("Loading previous messages...")
enableRadius: true
iconItem.small: true
}
}
Component.onCompleted: shortcuts.flickTarget = eventList
property string inviter: chat.roomInfo.inviter || ""
property real yPos: visibleArea.yPosition
property bool canLoad: true
property bool loading: false
property bool ownEventsOnRight:
width < theme.chat.eventList.ownEventsOnRightUnderWidth
function canCombine(item, itemAfter) {
if (! item || ! itemAfter) return false
return Boolean(
! canTalkBreak(item, itemAfter) &&
! canDayBreak(item, itemAfter) &&
item.sender_id === itemAfter.sender_id &&
utils.minutesBetween(item.date, itemAfter.date) <= 5
)
}
function canTalkBreak(item, itemAfter) {
if (! item || ! itemAfter) return false
return Boolean(
! canDayBreak(item, itemAfter) &&
utils.minutesBetween(item.date, itemAfter.date) >= 20
)
}
function canDayBreak(item, itemAfter) {
if (itemAfter && itemAfter.event_type === "RoomCreateEvent")
return true
if (! item || ! itemAfter || ! item.date || ! itemAfter.date)
return false
return item.date.getDate() !== itemAfter.date.getDate()
}
function loadPastEvents() {
// try/catch blocks to hide pyotherside error when the
// component is destroyed but func is still running
try {
eventList.canLoad = false
eventList.loading = true
py.callClientCoro(
chat.userId,
"load_past_events",
[chat.roomId],
moreToLoad => {
try {
eventList.canLoad = moreToLoad
// Call yPosChanged() to run this func again
// if the loaded messages aren't enough to fill
// the screen.
if (moreToLoad) yPosChanged()
eventList.loading = false
} catch (err) {
return
}
eventList.loading = false
} catch (err) {
return
}
)
} catch (err) {
return
}
}
)
} catch (err) {
return
}
model: ModelStore.get(chat.userId, chat.roomId, "events")
delegate: EventDelegate {}
}
}
@ -193,4 +142,49 @@ Rectangle {
visible: eventList.model.count < 1
anchors.fill: parent
}
DragHandler {
target: null
onActiveChanged: if (! active) dragFlicker.speed = 0
onCentroidChanged: {
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 boost = 20 * (pos < dist ? -pos : -(height - pos))
dragFlicker.speed =
left && vel && pos < dist ? 1000 + boost :
left && vel && pos > height - dist ? -1000 + -boost :
0
}
}
Timer {
id: dragFlicker
interval: 100
running: speed !== 0
repeat: true
onTriggered: {
if (eventList.verticalOvershoot !== 0) return
if (speed < 0 && eventList.atYEnd) return
if (eventList.atYBeggining) {
if (bouncedStart) { return } else { bouncedStart = true }
}
eventList.flick(0, speed * acceleration)
acceleration = Math.min(8, acceleration * 1.05)
}
onRunningChanged: if (! running) {
acceleration = 1.0
bouncedStart = false
eventList.cancelFlick()
eventList.returnToBounds()
}
property real speed: 0.0
property real acceleration: 1.0
property bool bouncedStart: false
}
}