Initial implementation of message selection

This commit is contained in:
miruka 2019-08-30 11:17:13 -04:00
parent aaaff814a1
commit 016f76167f
7 changed files with 348 additions and 101 deletions

View File

@ -1,3 +1,7 @@
- Message selection
- Make scroll wheel usable
- Copying messages (menu & shortcut)
- Refactoring - Refactoring
- Banners - Banners
- Composer - Composer

View File

@ -3,6 +3,7 @@ import QtQuick.Controls 2.12
ListView { ListView {
id: listView id: listView
interactive: enableFlicking
currentIndex: -1 currentIndex: -1
keyNavigationWraps: true keyNavigationWraps: true
highlightMoveDuration: theme.animationDuration highlightMoveDuration: theme.animationDuration
@ -13,6 +14,8 @@ ListView {
preferredHighlightEnd: height / 2 + currentItemHeight preferredHighlightEnd: height / 2 + currentItemHeight
property bool enableFlicking: true
readonly property int currentItemHeight: readonly property int currentItemHeight:
currentItem ? currentItem.height : 0 currentItem ? currentItem.height : 0
@ -21,7 +24,9 @@ ListView {
color: theme.controls.listView.highlight color: theme.controls.listView.highlight
} }
ScrollBar.vertical: ScrollBar { visible: listView.interactive } ScrollBar.vertical: ScrollBar {
visible: listView.interactive || ! listView.enableFlicking
}
add: Transition { add: Transition {
ParallelAnimation { ParallelAnimation {
@ -45,6 +50,5 @@ ListView {
} }
} }
populate: add
displaced: move displaced: move
} }

View File

@ -0,0 +1,119 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
TextEdit {
id: label
font.family: theme.fontFamily.sans
font.pixelSize: theme.fontSize.normal
color: theme.colors.text
textFormat: Label.PlainText
tabStopDistance: 4 * 4 // 4 spaces
readOnly: true
persistentSelection: true
onLinkActivated: Qt.openUrlExternally(link)
property HSelectableLabelContainer container
property int index
function updateSelection() {
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()
}
}
MouseArea {
anchors.fill: label
acceptedButtons: Qt.NoButton
cursorShape: label.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}

View File

@ -0,0 +1,72 @@
import QtQuick 2.12
Item {
signal deselectAll()
property bool reversed: false
readonly property bool dragging: pointHandler.active || dragHandler.active
// onDraggingChanged: print(dragging)
property bool selecting: false
property int selectionStart: -1
property int 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 alias dragPoint: dragHandler.centroid
readonly property alias dragPosition: dragHandler.centroid.position
function clearSelection() {
selecting = false
selectionStart = -1
selectionEnd = -1
selectionStartPosition = Qt.point(-1, -1)
selectionEndPosition = Qt.point(-1, -1)
deselectAll()
}
function copySelection() {
let toCopy = []
for (let key of Object.keys(selectedTexts).sort()) {
if (selectedTexts[key]) toCopy.push(selectedTexts[key])
}
// Call some function to copy to clipboard here instead
print("Copy: <" + toCopy.join("\n\n") + ">")
}
Item { id: dragPoint }
DragHandler {
id: dragHandler
target: dragPoint
onActiveChanged: {
if (active) {
target.Drag.active = true
} else {
target.Drag.drop()
target.Drag.active = false
selecting = false
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: clearSelection()
}
PointHandler {
id: pointHandler
}
}

View File

@ -8,15 +8,6 @@ Row {
spacing: theme.spacing / 2 spacing: theme.spacing / 2
readonly property string eventText: Utils.processedEventText(model) readonly property string eventText: Utils.processedEventText(model)
readonly property real lineHeight:
! eventText.match(/<img .+\/?>/) && multiline ? 1.25 : 1.0
readonly property bool multiline:
(eventText.match(/(\n|<br\/?>)/) || []).length > 0 ||
contentLabel.contentWidth < (
contentLabel.implicitWidth -
contentLabel.leftPadding -
contentLabel.rightPadding
)
Item { Item {
width: hideAvatar ? 0 : 48 width: hideAvatar ? 0 : 48
@ -64,16 +55,17 @@ Row {
textFormat: Text.StyledText textFormat: Text.StyledText
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: onRight ? Text.AlignRight : Text.AlignLeft horizontalAlignment: onRight ? Text.AlignRight : Text.AlignLeft
lineHeight: eventContent.lineHeight
leftPadding: theme.spacing leftPadding: theme.spacing
rightPadding: leftPadding rightPadding: leftPadding
topPadding: theme.spacing / 2 * lineHeight topPadding: theme.spacing / 2 * lineHeight
} }
HRichLabel { HSelectableLabel {
id: contentLabel id: contentLabel
width: parent.width width: parent.width
container: selectableLabelContainer
index: model.index
text: theme.chat.message.styleInclude + text: theme.chat.message.styleInclude +
eventContent.eventText + eventContent.eventText +
@ -87,14 +79,14 @@ Row {
"&nbsp;<font size=" + theme.fontSize.small + "&nbsp;<font size=" + theme.fontSize.small +
"px>⏳</font>" : "") "px>⏳</font>" : "")
lineHeight: eventContent.lineHeight
color: theme.chat.message.body color: theme.chat.message.body
wrapMode: Text.Wrap wrapMode: Text.Wrap
textFormat: Text.RichText
leftPadding: theme.spacing leftPadding: theme.spacing
rightPadding: leftPadding rightPadding: leftPadding
topPadding: nameLabel.visible ? 0 : bottomPadding topPadding: nameLabel.visible ? 0 : bottomPadding
bottomPadding: theme.spacing / 2 * lineHeight bottomPadding: theme.spacing / 2
} }
} }
} }

View File

@ -7,11 +7,87 @@ Rectangle {
color: theme.chat.eventList.background color: theme.chat.eventList.background
HSelectableLabelContainer {
id: selectableLabelContainer
anchors.fill: parent
reversed: eventList.verticalLayoutDirection == ListView.BottomToTop
onDragPositionChanged: {
let boost = 20 * (
dragPosition.y < 50 ?
-dragPosition.y : -(height - dragPosition.y)
)
dragFlicker.speed =
dragPosition.x == 0 && dragPosition.y == 0 ? 0 :
dragPosition.y < 50 ? 1000 + boost:
dragPosition.y > height - 50 ? -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
}
HListView { HListView {
id: eventList id: eventList
clip: true clip: true
enableFlicking: false
anchors.fill: parent
anchors.leftMargin: theme.spacing
anchors.rightMargin: theme.spacing
topMargin: theme.spacing
bottomMargin: theme.spacing
verticalLayoutDirection: ListView.BottomToTop
// Keep x scroll pages cached, to limit images having to be
// reloaded from network.
cacheBuffer: height * 4
onYPosChanged:
if (canLoad && yPos < 0.1) Qt.callLater(loadPastEvents)
// When an invited room becomes joined, we should now be able to
// fetch past events.
onInviterChanged: canLoad = true
Component.onCompleted: shortcuts.flickTarget = eventList Component.onCompleted: shortcuts.flickTarget = eventList
property string inviter: chatPage.roomInfo.inviter || ""
property real yPos: visibleArea.yPosition
property bool canLoad: true
property bool ownEventsOnRight:
width < theme.chat.eventList.ownEventsOnRightUnderWidth
function canCombine(item, itemAfter) { function canCombine(item, itemAfter) {
if (! item || ! itemAfter) return false if (! item || ! itemAfter) return false
@ -42,34 +118,6 @@ Rectangle {
return item.date.getDate() != itemAfter.date.getDate() return item.date.getDate() != itemAfter.date.getDate()
} }
model: HListModel {
keyField: "client_id"
source:
modelSources[["Event", chatPage.userId, chatPage.roomId]] || []
}
property bool ownEventsOnRight:
width < theme.chat.eventList.ownEventsOnRightUnderWidth
delegate: EventDelegate {}
anchors.fill: parent
anchors.leftMargin: theme.spacing
anchors.rightMargin: theme.spacing
topMargin: theme.spacing
bottomMargin: theme.spacing
verticalLayoutDirection: ListView.BottomToTop
// Keep x scroll pages cached, to limit images having to be
// reloaded from network.
cacheBuffer: height * 4
// Declaring this as "alias" provides the on... signal
property real yPos: visibleArea.yPosition
property bool canLoad: true
onYPosChanged: if (canLoad && yPos < 0.1) Qt.callLater(loadPastEvents)
function loadPastEvents() { function loadPastEvents() {
// try/catch blocks to hide pyotherside error when the // try/catch blocks to hide pyotherside error when the
// component is destroyed but func is still running // component is destroyed but func is still running
@ -92,10 +140,16 @@ Rectangle {
} }
} }
property string inviter: chatPage.roomInfo.inviter || ""
// When an invited room becomes joined, we should now be able to fetch model: HListModel {
// past events. keyField: "client_id"
onInviterChanged: canLoad = true source: modelSources[[
"Event", chatPage.userId, chatPage.roomId
]] || []
}
delegate: EventDelegate {}
}
} }
HNoticePage { HNoticePage {

View File

@ -169,8 +169,10 @@ function getItem(array, mainKey, value) {
} }
function smartVerticalFlick(flickable, baseVelocity, fastMultiply=3) { function smartVerticalFlick(flickable, baseVelocity, fastMultiply=4) {
if (! flickable.interactive) { return } if (! flickable.interactive && flickable.enableFlicking) return
if (flickable.verticalOvershoot != 0) return
if (baseVelocity > 0 && flickable.atYEnd) return
baseVelocity = -baseVelocity baseVelocity = -baseVelocity
let vel = -flickable.verticalVelocity let vel = -flickable.verticalVelocity