From 710dba09ec96dc8f05abddecef9869bc55c8cd4b Mon Sep 17 00:00:00 2001 From: miruka Date: Tue, 24 Mar 2020 11:26:17 -0400 Subject: [PATCH] Remove message text selection hack --- TODO.md | 6 +- src/gui/Base/HSelectableLabel.qml | 112 +------ src/gui/Base/HSelectableLabelContainer.qml | 92 ------ src/gui/Pages/Chat/Composer.qml | 26 +- src/gui/Pages/Chat/Timeline/EventContent.qml | 4 - src/gui/Pages/Chat/Timeline/EventList.qml | 300 +++++++++---------- 6 files changed, 174 insertions(+), 366 deletions(-) delete mode 100644 src/gui/Base/HSelectableLabelContainer.qml diff --git a/TODO.md b/TODO.md index a2fa0059..bf402a2b 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/src/gui/Base/HSelectableLabel.qml b/src/gui/Base/HSelectableLabel.qml index 33f00a86..5df02ac6 100644 --- a/src/gui/Base/HSelectableLabel.qml +++ b/src/gui/Base/HSelectableLabel.qml @@ -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 diff --git a/src/gui/Base/HSelectableLabelContainer.qml b/src/gui/Base/HSelectableLabelContainer.qml deleted file mode 100644 index 25e314a5..00000000 --- a/src/gui/Base/HSelectableLabelContainer.qml +++ /dev/null @@ -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 } -} diff --git a/src/gui/Pages/Chat/Composer.qml b/src/gui/Pages/Chat/Composer.qml index a574b3a0..972d8b77 100644 --- a/src/gui/Pages/Chat/Composer.qml +++ b/src/gui/Pages/Chat/Composer.qml @@ -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 && diff --git a/src/gui/Pages/Chat/Timeline/EventContent.qml b/src/gui/Pages/Chat/Timeline/EventContent.qml index 012012bb..9060b3b7 100644 --- a/src/gui/Pages/Chat/Timeline/EventContent.qml +++ b/src/gui/Pages/Chat/Timeline/EventContent.qml @@ -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 } diff --git a/src/gui/Pages/Chat/Timeline/EventList.qml b/src/gui/Pages/Chat/Timeline/EventList.qml index 14154ed7..7d69dd20 100644 --- a/src/gui/Pages/Chat/Timeline/EventList.qml +++ b/src/gui/Pages/Chat/Timeline/EventList.qml @@ -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 + } }