diff --git a/TODO.md b/TODO.md
index 341f267f..815658fe 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,3 +1,7 @@
+- Message selection
+ - Make scroll wheel usable
+ - Copying messages (menu & shortcut)
+
- Refactoring
- Banners
- Composer
diff --git a/src/qml/Base/HListView.qml b/src/qml/Base/HListView.qml
index f1a66802..1a778dd0 100644
--- a/src/qml/Base/HListView.qml
+++ b/src/qml/Base/HListView.qml
@@ -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
}
diff --git a/src/qml/Base/HSelectableLabel.qml b/src/qml/Base/HSelectableLabel.qml
new file mode 100644
index 00000000..d1e9675f
--- /dev/null
+++ b/src/qml/Base/HSelectableLabel.qml
@@ -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
+ }
+}
diff --git a/src/qml/Base/HSelectableLabelContainer.qml b/src/qml/Base/HSelectableLabelContainer.qml
new file mode 100644
index 00000000..12559513
--- /dev/null
+++ b/src/qml/Base/HSelectableLabelContainer.qml
@@ -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
+ }
+}
diff --git a/src/qml/Chat/Timeline/EventContent.qml b/src/qml/Chat/Timeline/EventContent.qml
index 70abd599..3faa1c8b 100644
--- a/src/qml/Chat/Timeline/EventContent.qml
+++ b/src/qml/Chat/Timeline/EventContent.qml
@@ -8,15 +8,6 @@ Row {
spacing: theme.spacing / 2
readonly property string eventText: Utils.processedEventText(model)
- readonly property real lineHeight:
- ! eventText.match(//) && multiline ? 1.25 : 1.0
- readonly property bool multiline:
- (eventText.match(/(\n|
)/) || []).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 {
" ⏳" : "")
- 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
}
}
}
diff --git a/src/qml/Chat/Timeline/EventList.qml b/src/qml/Chat/Timeline/EventList.qml
index 69ec24a3..c7d2b896 100644
--- a/src/qml/Chat/Timeline/EventList.qml
+++ b/src/qml/Chat/Timeline/EventList.qml
@@ -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 {
diff --git a/src/qml/utils.js b/src/qml/utils.js
index 27afddfd..4b5b435e 100644
--- a/src/qml/utils.js
+++ b/src/qml/utils.js
@@ -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