Initial implementation of message selection
This commit is contained in:
parent
aaaff814a1
commit
016f76167f
4
TODO.md
4
TODO.md
|
@ -1,3 +1,7 @@
|
|||
- Message selection
|
||||
- Make scroll wheel usable
|
||||
- Copying messages (menu & shortcut)
|
||||
|
||||
- Refactoring
|
||||
- Banners
|
||||
- Composer
|
||||
|
|
|
@ -3,6 +3,7 @@ import QtQuick.Controls 2.12
|
|||
|
||||
ListView {
|
||||
id: listView
|
||||
interactive: enableFlicking
|
||||
currentIndex: -1
|
||||
keyNavigationWraps: true
|
||||
highlightMoveDuration: theme.animationDuration
|
||||
|
@ -13,6 +14,8 @@ ListView {
|
|||
preferredHighlightEnd: height / 2 + currentItemHeight
|
||||
|
||||
|
||||
property bool enableFlicking: true
|
||||
|
||||
readonly property int currentItemHeight:
|
||||
currentItem ? currentItem.height : 0
|
||||
|
||||
|
@ -21,7 +24,9 @@ ListView {
|
|||
color: theme.controls.listView.highlight
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar { visible: listView.interactive }
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
visible: listView.interactive || ! listView.enableFlicking
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
ParallelAnimation {
|
||||
|
@ -45,6 +50,5 @@ ListView {
|
|||
}
|
||||
}
|
||||
|
||||
populate: add
|
||||
displaced: move
|
||||
}
|
||||
|
|
119
src/qml/Base/HSelectableLabel.qml
Normal file
119
src/qml/Base/HSelectableLabel.qml
Normal 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
|
||||
}
|
||||
}
|
72
src/qml/Base/HSelectableLabelContainer.qml
Normal file
72
src/qml/Base/HSelectableLabelContainer.qml
Normal 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
|
||||
}
|
||||
}
|
|
@ -8,15 +8,6 @@ Row {
|
|||
spacing: theme.spacing / 2
|
||||
|
||||
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 {
|
||||
width: hideAvatar ? 0 : 48
|
||||
|
@ -64,16 +55,17 @@ Row {
|
|||
textFormat: Text.StyledText
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: onRight ? Text.AlignRight : Text.AlignLeft
|
||||
lineHeight: eventContent.lineHeight
|
||||
|
||||
leftPadding: theme.spacing
|
||||
rightPadding: leftPadding
|
||||
topPadding: theme.spacing / 2 * lineHeight
|
||||
}
|
||||
|
||||
HRichLabel {
|
||||
HSelectableLabel {
|
||||
id: contentLabel
|
||||
width: parent.width
|
||||
container: selectableLabelContainer
|
||||
index: model.index
|
||||
|
||||
text: theme.chat.message.styleInclude +
|
||||
eventContent.eventText +
|
||||
|
@ -87,14 +79,14 @@ Row {
|
|||
" <font size=" + theme.fontSize.small +
|
||||
"px>⏳</font>" : "")
|
||||
|
||||
lineHeight: eventContent.lineHeight
|
||||
color: theme.chat.message.body
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
|
||||
leftPadding: theme.spacing
|
||||
rightPadding: leftPadding
|
||||
topPadding: nameLabel.visible ? 0 : bottomPadding
|
||||
bottomPadding: theme.spacing / 2 * lineHeight
|
||||
bottomPadding: theme.spacing / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,95 +7,149 @@ Rectangle {
|
|||
|
||||
color: theme.chat.eventList.background
|
||||
|
||||
HListView {
|
||||
id: eventList
|
||||
clip: true
|
||||
Component.onCompleted: shortcuts.flickTarget = eventList
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
model: HListModel {
|
||||
keyField: "client_id"
|
||||
source:
|
||||
modelSources[["Event", chatPage.userId, chatPage.roomId]] || []
|
||||
}
|
||||
|
||||
property bool ownEventsOnRight:
|
||||
width < theme.chat.eventList.ownEventsOnRightUnderWidth
|
||||
|
||||
delegate: EventDelegate {}
|
||||
|
||||
HSelectableLabelContainer {
|
||||
id: selectableLabelContainer
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: theme.spacing
|
||||
anchors.rightMargin: theme.spacing
|
||||
reversed: eventList.verticalLayoutDirection == ListView.BottomToTop
|
||||
|
||||
topMargin: theme.spacing
|
||||
bottomMargin: theme.spacing
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
onDragPositionChanged: {
|
||||
let boost = 20 * (
|
||||
dragPosition.y < 50 ?
|
||||
-dragPosition.y : -(height - dragPosition.y)
|
||||
)
|
||||
|
||||
// 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() {
|
||||
// try/catch blocks to hide pyotherside error when the
|
||||
// component is destroyed but func is still running
|
||||
|
||||
try {
|
||||
eventList.canLoad = false
|
||||
|
||||
py.callClientCoro(
|
||||
chatPage.userId, "load_past_events", [chatPage.roomId],
|
||||
moreToLoad => {
|
||||
try {
|
||||
eventList.canLoad = moreToLoad
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
dragFlicker.speed =
|
||||
dragPosition.x == 0 && dragPosition.y == 0 ? 0 :
|
||||
dragPosition.y < 50 ? 1000 + boost:
|
||||
dragPosition.y > height - 50 ? -1000 + -boost :
|
||||
0
|
||||
}
|
||||
|
||||
property string inviter: chatPage.roomInfo.inviter || ""
|
||||
// When an invited room becomes joined, we should now be able to fetch
|
||||
// past events.
|
||||
onInviterChanged: canLoad = true
|
||||
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 {
|
||||
id: eventList
|
||||
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
|
||||
|
||||
|
||||
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) {
|
||||
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
|
||||
|
||||
py.callClientCoro(
|
||||
chatPage.userId, "load_past_events", [chatPage.roomId],
|
||||
moreToLoad => {
|
||||
try {
|
||||
eventList.canLoad = moreToLoad
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
model: HListModel {
|
||||
keyField: "client_id"
|
||||
source: modelSources[[
|
||||
"Event", chatPage.userId, chatPage.roomId
|
||||
]] || []
|
||||
}
|
||||
|
||||
delegate: EventDelegate {}
|
||||
}
|
||||
}
|
||||
|
||||
HNoticePage {
|
||||
|
|
|
@ -169,8 +169,10 @@ function getItem(array, mainKey, value) {
|
|||
}
|
||||
|
||||
|
||||
function smartVerticalFlick(flickable, baseVelocity, fastMultiply=3) {
|
||||
if (! flickable.interactive) { return }
|
||||
function smartVerticalFlick(flickable, baseVelocity, fastMultiply=4) {
|
||||
if (! flickable.interactive && flickable.enableFlicking) return
|
||||
if (flickable.verticalOvershoot != 0) return
|
||||
if (baseVelocity > 0 && flickable.atYEnd) return
|
||||
|
||||
baseVelocity = -baseVelocity
|
||||
let vel = -flickable.verticalVelocity
|
||||
|
|
Loading…
Reference in New Issue
Block a user