Refactor Composer

- Have a simple HTextArea component instead of HScrollTextArea
- Split composer parts between multiple files
This commit is contained in:
miruka 2020-05-29 16:22:53 -04:00
parent a87cbd3bac
commit a91a0c18f7
8 changed files with 413 additions and 283 deletions

View File

@ -7,12 +7,12 @@
- Use new default/reset controls system
- Display name field text should be colored
- Split `HScrollableTextArea` into `HTextArea` and `HScrollView` components
- Refactor `Composer`
- Drop the `HBox` `buttonModel`/`buttonCallbacks` `HBox` approach,
be more declarative
- Reorder QML object declarations,
conform to https://doc-snapshots.qt.io/qt5-dev/qml-codingconventions.html
## Issues
- SSL error on python 3.7

View File

@ -1,6 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
@ -25,7 +24,6 @@ ScrollView {
property var saveId: "ALL"
property var saveProperties: ["text"]
property alias backgroundColor: textAreaBackground.color
property alias placeholderText: textArea.placeholderText
property alias placeholderTextColor: textArea.placeholderTextColor
property alias area: textArea

108
src/gui/Base/HTextArea.qml Normal file
View File

@ -0,0 +1,108 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
TextArea {
id: textArea
property string saveName: ""
property var saveId: "ALL"
property var saveProperties: ["text"]
property var focusItemOnTab: null
property var disabledText: null
property string defaultText: ""
readonly property bool changed: text !== defaultText
property alias backgroundColor: textAreaBackground.color
function reset() { clear(); text = defaultText }
function append(text) { insert(cursorPosition, text) }
text: defaultText
opacity: enabled ? 1 : theme.disabledElementsOpacity
selectByMouse: true
leftPadding: theme.spacing
rightPadding: leftPadding
topPadding: theme.spacing / 1.5
bottomPadding: topPadding
readOnly: ! visible
wrapMode: TextEdit.Wrap
font.family: theme.fontFamily.sans
font.pixelSize: theme.fontSize.normal
font.pointSize: -1
placeholderTextColor: theme.controls.textArea.placeholderText
color: theme.controls.textArea.text
background: Rectangle {
id: textAreaBackground
color: theme.controls.textArea.background
radius: theme.radius
}
// Set it only on component creation to avoid binding loops
Component.onCompleted: if (! text) {
text = window.getState(this, "text", "")
textArea.cursorPosition = text.length
}
onTextChanged: window.saveState(this)
Keys.onPressed: if (
event.modifiers & Qt.AltModifier ||
event.modifiers & Qt.MetaModifier
) event.accepted = true
KeyNavigation.priority: KeyNavigation.BeforeItem
KeyNavigation.tab: focusItemOnTab
Binding on color {
value: "transparent"
when: disabledText !== null && ! textArea.enabled
}
Binding on placeholderTextColor {
value: "transparent"
when: disabledText !== null && ! textArea.enabled
}
Binding on implicitHeight {
value: disabledTextLabel.implicitHeight
when: disabledText !== null && ! textArea.enabled
}
Behavior on opacity { HNumberAnimation {} }
Behavior on color { HColorAnimation {} }
Behavior on placeholderTextColor { HColorAnimation {} }
HLabel {
id: disabledTextLabel
anchors.fill: parent
visible: opacity > 0
opacity: disabledText !== null && parent.enabled ? 0 : 1
text: disabledText || ""
leftPadding: parent.leftPadding
rightPadding: parent.rightPadding
topPadding: parent.topPadding
bottomPadding: parent.bottomPadding
wrapMode:
parent.wrapMode === TextEdit.Wrap ? Text.Wrap :
parent.wrapMode === TextEdit.WordWrap ? Text.WordWrap :
parent.wrapMode === TextEdit.WrapAnywhere ? Text.WrapAnywhere :
Text.NoWrap
font.family: parent.font.family
font.pixelSize: parent.font.pixelSize
Behavior on opacity { HNumberAnimation {} }
}
}

View File

@ -4,8 +4,9 @@ import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
import "Banners"
import "Timeline"
import "Composer"
import "FileTransfer"
import "Timeline"
HColumnPage {
id: chatPage
@ -101,6 +102,7 @@ HColumnPage {
LeftBanner {
id: leftBanner
visible: chat.roomInfo.left
Layout.fillWidth: true
}
@ -109,6 +111,8 @@ HColumnPage {
eventList: loadEventList ? eventListLoader.item.eventList : null
visible:
! chat.roomInfo.left && ! chat.roomInfo.inviter_id
}
Layout.fillWidth: true
Layout.maximumHeight: parent.height / 2
}
}

View File

@ -1,276 +0,0 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import Clipboard 0.1
import CppUtils 0.1
import "../.."
import "../../Base"
import "../../Dialogs"
Rectangle {
id: composer
color: theme.chat.composer.background
Layout.fillWidth: true
Layout.minimumHeight: theme.baseElementsHeight
Layout.preferredHeight: areaScrollView.implicitHeight
Layout.maximumHeight: pageLoader.height / 2
property HListView eventList
property string indent: " "
property var aliases: {
const obj = {}
for (const [id, alia] of Object.entries(window.settings.writeAliases)){
const room = ModelStore.get(id, "rooms").find(chat.roomId)
if (room &&
! room.inviter_id && ! room.left && room.can_send_messages)
obj[id] = alia
}
return obj
}
property string toSend: ""
property string writingUserId: chat.userId
property QtObject writingUserInfo:
ModelStore.get("accounts").find(writingUserId)
property bool textChangedSinceLostFocus: false
property alias textArea: areaScrollView.area
readonly property int cursorPosition:
textArea.cursorPosition
readonly property int cursorY:
textArea.text.substring(0, cursorPosition).split("\n").length - 1
readonly property int cursorX:
cursorPosition - lines.slice(0, cursorY).join("").length - cursorY
readonly property var lines: textArea.text.split("\n")
readonly property string lineText: lines[cursorY] || ""
readonly property string lineTextUntilCursor:
lineText.substring(0, cursorX)
// readonly property int deleteCharsOnBackspace:
// lineTextUntilCursor.match(/^ +$/) ?
// lineTextUntilCursor.match(/ {1,4}/g).slice(-1)[0].length :
// 1
function takeFocus() { areaScrollView.forceActiveFocus() }
HRowLayout {
anchors.fill: parent
HUserAvatar {
id: avatar
userId: writingUserId
displayName: writingUserInfo ? writingUserInfo.display_name : ""
mxc: writingUserInfo ? writingUserInfo.avatar_url : ""
radius: 0
}
HScrollableTextArea {
id: areaScrollView
saveName: "composer"
saveId: [chat.roomId, writingUserId]
enabled: chat.roomInfo.can_send_messages
disabledText:
qsTr("You do not have permission to post in this room")
placeholderText: qsTr("Type a message...")
backgroundColor: "transparent"
area.tabStopDistance: 4 * 4 // 4 spaces
area.focus: true
Layout.fillHeight: true
Layout.fillWidth: true
function setTyping(typing) {
py.callClientCoro(
writingUserId,
"room_typing",
[chat.roomId, typing, 5000]
)
}
onTextChanged: {
if (utils.isEmptyObject(aliases)) {
writingUserId = Qt.binding(() => chat.userId)
toSend = text
setTyping(Boolean(text))
textChangedSinceLostFocus = true
return
}
let foundAlias = null
for (const [user, writing_alias] of Object.entries(aliases)) {
if (text.startsWith(writing_alias + " ")) {
writingUserId = user
foundAlias = new RegExp("^" + writing_alias + " ")
break
}
}
if (foundAlias) {
toSend = text.replace(foundAlias, "")
setTyping(Boolean(text))
textChangedSinceLostFocus = true
return
}
writingUserId = Qt.binding(() => chat.userId)
toSend = text
const vals = Object.values(aliases)
const longestAlias =
vals.reduce((a, b) => a.length > b.length ? a: b)
const textNotStartsWithAnyAlias =
! vals.some(a => a.startsWith(text))
const textContainsCharNotInAnyAlias =
vals.every(a => text.split("").some(c => ! a.includes(c)))
// Only set typing when it's sure that the user will not use
// an alias and has written something
if (toSend &&
(text.length > longestAlias.length ||
textNotStartsWithAnyAlias ||
textContainsCharNotInAnyAlias))
{
setTyping(Boolean(text))
textChangedSinceLostFocus = true
}
}
area.onEditingFinished: { // when lost focus
if (text && textChangedSinceLostFocus) {
setTyping(false)
textChangedSinceLostFocus = false
}
}
Component.onCompleted: {
area.Keys.onEscapePressed.connect(ev => {
if (chat.replyToEventId) {
chat.replyToEventId = ""
chat.replyToUserId = ""
chat.replyToDisplayName = ""
}
})
area.Keys.onReturnPressed.connect(ev => {
ev.accepted = true
if (ev.modifiers & Qt.ShiftModifier ||
ev.modifiers & Qt.ControlModifier ||
ev.modifiers & Qt.AltModifier)
{
let indents = 0
const parts = lineText.split(indent)
for (const [i, part] of parts.entries()) {
if (i === parts.length - 1 || part) { break }
indents += 1
}
const add = indent.repeat(indents)
textArea.insert(cursorPosition, "\n" + add)
return
}
if (textArea.text === "") { return }
const args = [chat.roomId, toSend, chat.replyToEventId]
py.callClientCoro(writingUserId, "send_text", args)
area.clear()
chat.replyToEventId = ""
chat.replyToUserId = ""
chat.replyToDisplayName = ""
})
area.Keys.onEnterPressed.connect(area.Keys.onReturnPressed)
area.Keys.onTabPressed.connect(ev => {
ev.accepted = true
textArea.insert(cursorPosition, indent)
})
area.Keys.onPressed.connect(ev => {
if (ev.matches(StandardKey.Copy) &&
! area.selectedText &&
eventList &&
(eventList.selectedCount ||
eventList.currentIndex !== -1)) {
ev.accepted = true
eventList.copySelectedDelegates()
return
}
// FIXME: buggy
// if (ev.modifiers === Qt.NoModifier &&
// ev.key === Qt.Key_Backspace &&
// ! textArea.selectedText)
// {
// ev.accepted = true
// textArea.remove(
// cursorPosition - deleteCharsOnBackspace,
// cursorPosition
// )
// }
})
}
}
HButton {
enabled: chat.roomInfo.can_send_messages
icon.name: "upload-file"
backgroundColor: theme.chat.composer.uploadButton.background
toolTip.text:
chat.userInfo.max_upload_size ?
qsTr("Send files (%1 max)").arg(
CppUtils.formattedBytes(chat.userInfo.max_upload_size, 0),
) :
qsTr("Send files")
onClicked: sendFilePicker.dialog.open()
Layout.fillHeight: true
HShortcut {
sequences: window.settings.keys.sendFileFromPathInClipboard
onActivated: utils.sendFile(
chat.userId, chat.roomId, Clipboard.text.trim(),
)
}
SendFilePicker {
id: sendFilePicker
userId: chat.userId
roomId: chat.roomId
HShortcut {
sequences: window.settings.keys.sendFile
onActivated: sendFilePicker.dialog.open()
}
}
}
}
}

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import "../../../Base"
Rectangle {
property alias eventList: messageArea.eventList
function takeFocus() { messageArea.forceActiveFocus() }
implicitHeight:
Math.max(theme.baseElementsHeight, messageArea.implicitHeight)
color: theme.chat.composer.background
HRowLayout {
anchors.fill: parent
HUserAvatar {
id: avatar
radius: 0
userId: messageArea.writingUserId
mxc:
messageArea.writingUserInfo ?
messageArea.writingUserInfo.avatar_url :
""
displayName:
messageArea.writingUserInfo ?
messageArea.writingUserInfo.display_name :
""
}
ScrollView {
Layout.fillHeight: true
Layout.fillWidth: true
MessageArea { id: messageArea}
}
UploadButton {
Layout.fillHeight: true
}
}
}

View File

@ -0,0 +1,205 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import "../../.."
import "../../../Base"
HTextArea {
id: textArea
property HListView eventList
property string indent: " "
property string toSend: ""
property bool textChangedSinceLostFocus: false
property string writingUserId: chat.userId
readonly property QtObject writingUserInfo:
ModelStore.get("accounts").find(writingUserId)
readonly property int cursorY:
textArea.text.substring(0, cursorPosition).split("\n").length - 1
readonly property int cursorX:
cursorPosition - lines.slice(0, cursorY).join("").length - cursorY
readonly property var lines: textArea.text.split("\n")
readonly property string lineText: lines[cursorY] || ""
readonly property string lineTextUntilCursor:
lineText.substring(0, cursorX)
// readonly property int deleteCharsOnBackspace:
// lineTextUntilCursor.match(/^ +$/) ?
// lineTextUntilCursor.match(/ {1,4}/g).slice(-1)[0].length :
// 1
readonly property var usableAliases: {
const obj = {}
// Get accounts that are members of this room with permission to talk
for (const [id, alia] of Object.entries(window.settings.writeAliases)){
const room = ModelStore.get(id, "rooms").find(chat.roomId)
if (room &&
! room.inviter_id && ! room.left && room.can_send_messages)
obj[id] = alia
}
return obj
}
function setTyping(typing) {
py.callClientCoro(
writingUserId, "room_typing", [chat.roomId, typing, 5000],
)
}
function clearReplyTo() {
if (! chat.replyToEventId) return
chat.replyToEventId = ""
chat.replyToUserId = ""
chat.replyToDisplayName = ""
}
function addNewLine() {
let indents = 0
const parts = lineText.split(indent)
for (const [i, part] of parts.entries()) {
if (i === parts.length - 1 || part) { break }
indents += 1
}
const add = indent.repeat(indents)
textArea.append("\n" + add)
}
function sendText() {
if (! toSend) return
const args = [chat.roomId, toSend, chat.replyToEventId]
py.callClientCoro(writingUserId, "send_text", args)
textArea.clear()
clearReplyTo()
}
saveName: "composer"
saveId: [chat.roomId, writingUserId]
enabled: chat.roomInfo.can_send_messages
disabledText: qsTr("You do not have permission to post in this room")
placeholderText: qsTr("Type a message...")
backgroundColor: "transparent"
tabStopDistance: 4 * 4 // 4 spaces
focus: true
// TODO: make this more declarative
onTextChanged: {
if (utils.isEmptyObject(usableAliases)) {
writingUserId = Qt.binding(() => chat.userId)
toSend = text
setTyping(Boolean(text))
textChangedSinceLostFocus = true
return
}
let foundAlias = null
for (const [user, writing_alias] of Object.entries(usableAliases)) {
if (text.startsWith(writing_alias + " ")) {
writingUserId = user
foundAlias = new RegExp("^" + writing_alias + " ")
break
}
}
if (foundAlias) {
toSend = text.replace(foundAlias, "")
setTyping(Boolean(text))
textChangedSinceLostFocus = true
return
}
writingUserId = Qt.binding(() => chat.userId)
toSend = text
const vals = Object.values(usableAliases)
const longestAlias =
vals.reduce((a, b) => a.length > b.length ? a: b)
const textNotStartsWithAnyAlias =
! vals.some(a => a.startsWith(text))
const textContainsCharNotInAnyAlias =
vals.every(a => text.split("").some(c => ! a.includes(c)))
// Only set typing when it's sure that the user will not use
// an alias and has written something
if (toSend &&
(text.length > longestAlias.length ||
textNotStartsWithAnyAlias ||
textContainsCharNotInAnyAlias))
{
setTyping(Boolean(text))
textChangedSinceLostFocus = true
}
}
onEditingFinished: { // when focus is lost
if (text && textChangedSinceLostFocus) {
setTyping(false)
textChangedSinceLostFocus = false
}
}
Keys.onEscapePressed: clearReplyTo()
Keys.onReturnPressed: ev => {
ev.accepted = true
ev.modifiers & Qt.ShiftModifier ||
ev.modifiers & Qt.ControlModifier ||
ev.modifiers & Qt.AltModifier ?
addNewLine() :
sendText()
}
Keys.onEnterPressed: ev => Keys.returnPressed(ev)
Keys.onTabPressed: ev => {
ev.accepted = true
textArea.append(indent)
}
Keys.onPressed: ev => {
if (ev.matches(StandardKey.Copy) &&
! textArea.selectedText &&
eventList &&
(eventList.selectedCount || eventList.currentIndex !== -1))
{
ev.accepted = true
eventList.copySelectedDelegates()
return
}
// FIXME: buggy
// if (ev.modifiers === Qt.NoModifier &&
// ev.key === Qt.Key_Backspace &&
// ! textArea.selectedText)
// {
// ev.accepted = true
// textArea.remove(
// cursorPosition - deleteCharsOnBackspace,
// cursorPosition
// )
// }
}
}

View File

@ -0,0 +1,40 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import Clipboard 0.1
import CppUtils 0.1
import "../../../Base"
import "../../../Dialogs"
HButton {
enabled: chat.roomInfo.can_send_messages
icon.name: "upload-file"
backgroundColor: theme.chat.composer.uploadButton.background
toolTip.text:
chat.userInfo.max_upload_size ?
qsTr("Send files (%1 max)").arg(
CppUtils.formattedBytes(chat.userInfo.max_upload_size, 0),
) :
qsTr("Send files")
onClicked: sendFilePicker.dialog.open()
HShortcut {
sequences: window.settings.keys.sendFileFromPathInClipboard
onActivated: utils.sendFile(
chat.userId, chat.roomId, Clipboard.text.trim(),
)
}
SendFilePicker {
id: sendFilePicker
userId: chat.userId
roomId: chat.roomId
HShortcut {
sequences: window.settings.keys.sendFile
onActivated: sendFilePicker.dialog.open()
}
}
}