Refactor EventContent
Use proper layouts and get rid of multiple annoyances/bugs like text-wrapping randomly happening in the middle of words, background rectangles being too large, a certain binding loop, etc
This commit is contained in:
parent
4c4d747ecf
commit
d20ab5a348
15
TODO.md
15
TODO.md
|
@ -6,8 +6,8 @@
|
|||
- Bottom/top bar
|
||||
- Uploading (+local echo)
|
||||
- Deduplicate uploads
|
||||
- Files, links, video, audio
|
||||
- File thumbnails, ask matrix API?
|
||||
- EventLink
|
||||
- File thumbnails + ask matrix API?
|
||||
- Encrypted media
|
||||
- Loading animation
|
||||
- GIF thumbnails: load the real animated image
|
||||
|
@ -19,8 +19,13 @@
|
|||
- Video: missing buttons and small size problems
|
||||
- Audio: online playback is buggy, must download+play file
|
||||
|
||||
- Refactor EventContent
|
||||
- No background/padding around medias
|
||||
- With this as eventText: `https://0x0.st/ztXe.png`, shrinking the window
|
||||
near its minimum size (seen at 262px) makes the image preview
|
||||
left padding wrong
|
||||
- In the "Leave me" room, "join > Hi > left" aren't combined
|
||||
- GIF glitchy border
|
||||
- Combined pure media events time looks bad
|
||||
- Avatars shouldn't be vertically centered
|
||||
|
||||
- Copy to X11 selection with new CppUtils class
|
||||
|
||||
|
@ -41,7 +46,7 @@
|
|||
- When qml syntax highlighting supports ES6 string interpolation, use that
|
||||
|
||||
- Fixes
|
||||
- GIF glitchy border
|
||||
- Event delegates changing height don't scroll the list
|
||||
- When selecting text and scrolling up, selection stops working after a while
|
||||
- Ensure all the text that should be copied is copied
|
||||
|
||||
|
|
|
@ -44,6 +44,5 @@ def guess_mime(file: IO) -> Optional[str]:
|
|||
|
||||
def plain2html(text: str) -> str:
|
||||
return html.escape(text)\
|
||||
.replace(" ", " ")\
|
||||
.replace("\n", "<br>")\
|
||||
.replace("\t", " " * 4)
|
||||
|
|
|
@ -9,7 +9,19 @@ Repeater {
|
|||
let total = 0
|
||||
|
||||
for (let i = 0; i < repeater.count; i++) {
|
||||
total += repeater.itemAt(i).implicitWidth
|
||||
let item = repeater.itemAt(i)
|
||||
if (item && item.width) total += item.width
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
readonly property int childrenWidth: {
|
||||
let total = 0
|
||||
|
||||
for (let i = 0; i < repeater.count; i++) {
|
||||
let item = repeater.itemAt(i)
|
||||
if (item && item.width) total += item.width
|
||||
}
|
||||
|
||||
return total
|
||||
|
|
|
@ -7,10 +7,6 @@ import "../../utils.js" as Utils
|
|||
|
||||
AudioPlayer {
|
||||
id: audio
|
||||
width: Math.min(
|
||||
mainColumn.width - eventContent.spacing * 2,
|
||||
theme.chat.message.audioWidth,
|
||||
)
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged:
|
||||
|
|
|
@ -3,125 +3,100 @@ import QtQuick.Layouts 1.12
|
|||
import "../../Base"
|
||||
import "../../utils.js" as Utils
|
||||
|
||||
Row {
|
||||
HRowLayout {
|
||||
id: eventContent
|
||||
spacing: theme.spacing / 1.25
|
||||
layoutDirection: onRight ? Qt.RightToLeft: Qt.LeftToRight
|
||||
|
||||
|
||||
readonly property string eventText: Utils.processedEventText(model)
|
||||
readonly property string eventTime: Utils.formatTime(model.date, false)
|
||||
readonly property bool pureMedia: ! eventText && previewLinksRepeater.count
|
||||
|
||||
readonly property string hoveredLink:
|
||||
nameLabel.hoveredLink || contentLabel.hoveredLink
|
||||
readonly property string hoveredLink: contentLabel.hoveredLink
|
||||
readonly property bool hoveredSelectable: contentHover.hovered
|
||||
|
||||
readonly property bool hoveredSelectable:
|
||||
nameHover.hovered || contentHover.hovered
|
||||
readonly property int messageBodyWidth:
|
||||
width - (avatarWrapper.visible ? avatarWrapper.width : 0) -
|
||||
totalSpacing
|
||||
|
||||
|
||||
Item {
|
||||
width: hideAvatar ? 0 : 58
|
||||
height: hideAvatar ? 0 : collapseAvatar ? 1 : smallAvatar ? 28 : 58
|
||||
opacity: hideAvatar || collapseAvatar ? 0 : 1
|
||||
visible: width > 0
|
||||
id: avatarWrapper
|
||||
opacity: collapseAvatar ? 0 : 1
|
||||
visible: ! hideAvatar
|
||||
|
||||
Layout.minimumWidth: 58
|
||||
Layout.minimumHeight: collapseAvatar ? 1 : smallAvatar ? 28 : 58
|
||||
Layout.maximumWidth: Layout.minimumWidth
|
||||
Layout.maximumHeight: Layout.minimumHeight
|
||||
|
||||
HUserAvatar {
|
||||
id: avatar
|
||||
userId: model.sender_id
|
||||
displayName: model.sender_name
|
||||
avatarUrl: model.sender_avatar
|
||||
width: hideAvatar ? 0 : 58
|
||||
height: hideAvatar ? 0 : collapseAvatar ? 1 : 58
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: isOwn?
|
||||
theme.chat.message.ownBackground :
|
||||
theme.chat.message.background
|
||||
|
||||
//width: nameLabel.implicitWidth
|
||||
width: Math.min(
|
||||
eventList.width - avatar.width - eventContent.spacing,
|
||||
theme.fontSize.normal * 0.5 * 75, // 600 with 16px font
|
||||
|
||||
Math.max(
|
||||
nameLabel.visible ? (nameLabel.implicitWidth + 1) : 0,
|
||||
|
||||
contentLabel.implicitWidth + 1,
|
||||
|
||||
previewLinksRepeater.count > 0 ?
|
||||
theme.chat.message.thumbnailWidth : 0,
|
||||
)
|
||||
)
|
||||
height: childrenRect.height
|
||||
y: parent.height / 2 - height / 2
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: parent.width
|
||||
spacing: theme.spacing / 1.75
|
||||
topPadding: theme.spacing / 1.75
|
||||
bottomPadding: topPadding
|
||||
|
||||
HSelectableLabel {
|
||||
id: nameLabel
|
||||
width: parent.width
|
||||
visible: ! hideNameLine
|
||||
container: selectableLabelContainer
|
||||
selectable: ! unselectableNameLine
|
||||
leftPadding: eventContent.spacing
|
||||
rightPadding: leftPadding
|
||||
|
||||
// This is +0.1 and content is +0 instead of the opposite,
|
||||
// because the eventList is reversed
|
||||
index: model.index + 0.1
|
||||
|
||||
text: Utils.coloredNameHtml(model.sender_name, model.sender_id)
|
||||
textFormat: Text.RichText
|
||||
wrapMode: Text.Wrap
|
||||
// horizontalAlignment: onRight ? Text.AlignRight : Text.AlignLeft
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
|
||||
function selectAllText() {
|
||||
// select the sender name, body and date
|
||||
container.clearSelection()
|
||||
nameLabel.selectAll()
|
||||
contentLabel.selectAll()
|
||||
contentLabel.updateContainerSelectedTexts()
|
||||
height: collapseAvatar ? 1 : 58
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: nameHover }
|
||||
}
|
||||
HColumnLayout {
|
||||
id: contentColumn
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
HSelectableLabel {
|
||||
id: contentLabel
|
||||
visible: Boolean(eventContent.eventText)
|
||||
width: parent.width
|
||||
container: selectableLabelContainer
|
||||
index: model.index
|
||||
|
||||
topPadding: theme.spacing / 1.75
|
||||
bottomPadding: topPadding
|
||||
leftPadding: eventContent.spacing
|
||||
rightPadding: leftPadding
|
||||
bottomPadding: previewLinksRepeater.count > 0 ?
|
||||
mainColumn.bottomPadding : 0
|
||||
|
||||
text: theme.chat.message.styleInclude +
|
||||
color: theme.chat.message.body
|
||||
wrapMode: TextEdit.Wrap
|
||||
textFormat: Text.RichText
|
||||
text:
|
||||
// CSS
|
||||
theme.chat.message.styleInclude +
|
||||
|
||||
// Sender name
|
||||
(hideNameLine ? "" : (
|
||||
"<div class='sender'>" +
|
||||
Utils.coloredNameHtml(model.sender_name, model.sender_id) +
|
||||
"</div>")) +
|
||||
|
||||
// Message body
|
||||
eventContent.eventText +
|
||||
// time
|
||||
// for some reason, if there's only one space,
|
||||
|
||||
// Time
|
||||
// For some reason, if there's only one space,
|
||||
// times will be on their own lines most of the time.
|
||||
" " +
|
||||
"<font size=" + theme.fontSize.small +
|
||||
"px color=" + theme.chat.message.date + '>' +
|
||||
eventTime +
|
||||
"</font>" +
|
||||
// local echo icon
|
||||
|
||||
// Local echo icon
|
||||
(model.is_local_echo ?
|
||||
" <font size=" + theme.fontSize.small +
|
||||
"px>⏳</font>" : "")
|
||||
|
||||
color: theme.chat.message.body
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
transform: Translate {
|
||||
x: onRight ?
|
||||
contentLabel.width - contentLabel.paintedWidth -
|
||||
contentLabel.leftPadding - contentLabel.rightPadding :
|
||||
0
|
||||
}
|
||||
|
||||
Layout.maximumWidth: Math.min(
|
||||
// 600px with 16px font
|
||||
theme.fontSize.normal * 0.5 * 75,
|
||||
messageBodyWidth - leftPadding - rightPadding,
|
||||
)
|
||||
|
||||
function selectAllText() {
|
||||
// Select the message body without the date or name
|
||||
|
@ -135,17 +110,66 @@ Row {
|
|||
}
|
||||
|
||||
HoverHandler { id: contentHover }
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(
|
||||
parent.paintedWidth +
|
||||
parent.leftPadding + parent.rightPadding,
|
||||
|
||||
previewLinksRepeater.childrenWidth +
|
||||
(pureMedia ? 0 : parent.leftPadding + parent.rightPadding),
|
||||
)
|
||||
height: contentColumn.height
|
||||
z: -1
|
||||
color: isOwn?
|
||||
theme.chat.message.ownBackground :
|
||||
theme.chat.message.background
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
HRepeater {
|
||||
id: previewLinksRepeater
|
||||
model: eventDelegate.currentItem.links
|
||||
|
||||
EventMediaLoader {
|
||||
info: eventDelegate.currentItem
|
||||
mediaUrl: modelData
|
||||
|
||||
Layout.bottomMargin: contentLabel.bottomPadding * multiply
|
||||
Layout.leftMargin: contentLabel.leftPadding * multiply
|
||||
Layout.rightMargin: contentLabel.rightPadding * multiply
|
||||
|
||||
Layout.minimumWidth:
|
||||
type === EventDelegate.Media.File ?
|
||||
theme.chat.message.fileMinWidth : -1
|
||||
|
||||
Layout.preferredWidth:
|
||||
type === EventDelegate.Media.Image ?
|
||||
(item ? item.fitSize.width : 0) :
|
||||
|
||||
type === EventDelegate.Media.Video ?
|
||||
theme.chat.message.videoWidth :
|
||||
|
||||
type === EventDelegate.Media.Audio ?
|
||||
theme.chat.message.audioWidth :
|
||||
|
||||
-1
|
||||
|
||||
Layout.maximumWidth:
|
||||
messageBodyWidth - Layout.leftMargin - Layout.rightMargin
|
||||
|
||||
Layout.maximumHeight:
|
||||
type === EventDelegate.Media.Image && item ?
|
||||
Utils.fitSize(
|
||||
Layout.maximumWidth,
|
||||
item.fitSize.height,
|
||||
Layout.maximumWidth
|
||||
).height : -1
|
||||
|
||||
readonly property int multiply: pureMedia ? 0 : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
}
|
||||
|
|
|
@ -48,9 +48,6 @@ Column {
|
|||
onRight ||
|
||||
combine
|
||||
|
||||
readonly property bool unselectableNameLine:
|
||||
hideNameLine && ! (onRight && ! combine)
|
||||
|
||||
readonly property int cursorShape:
|
||||
eventContent.hoveredLink || hoveredMediaTypeUrl.length > 0 ?
|
||||
Qt.PointingHandCursor :
|
||||
|
@ -96,7 +93,7 @@ Column {
|
|||
|
||||
EventContent {
|
||||
id: eventContent
|
||||
x: onRight ? parent.width - width : 0
|
||||
width: parent.width
|
||||
|
||||
Behavior on x { HNumberAnimation {} }
|
||||
}
|
||||
|
|
|
@ -3,11 +3,6 @@ import QtQuick.Layouts 1.12
|
|||
import "../../Base"
|
||||
|
||||
HTile {
|
||||
width: Math.min(
|
||||
mainColumn.width - eventContent.spacing * 2,
|
||||
theme.chat.message.thumbnailWidth,
|
||||
)
|
||||
|
||||
onLeftClicked: Qt.openUrlExternally(fileUrl)
|
||||
onRightClicked: eventDelegate.openContextMenu()
|
||||
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
import QtQuick 2.12
|
||||
import "../../Base"
|
||||
import "../../utils.js" as Utils
|
||||
|
||||
HImage {
|
||||
id: image
|
||||
sourceSize.width: theme.chat.message.thumbnailWidth
|
||||
sourceSize.height: theme.chat.message.thumbnailWidth
|
||||
width: Math.min(
|
||||
mainColumn.width - eventContent.spacing * 2,
|
||||
implicitWidth,
|
||||
theme.chat.message.thumbnailWidth,
|
||||
)
|
||||
width: fitSize.width
|
||||
height: fitSize.height
|
||||
|
||||
// Leaving PreserveAspectFit creates a binding loop, and is uneeded
|
||||
// since we calculate ourself the right size.
|
||||
fillMode: Image.Pad
|
||||
|
||||
|
||||
// source = thumbnail, fullSource = full original image
|
||||
property url fullSource: source
|
||||
|
||||
readonly property size fitSize: Utils.fitSize(
|
||||
implicitWidth, implicitHeight, theme.chat.message.thumbnailWidth,
|
||||
)
|
||||
|
||||
|
||||
TapHandler {
|
||||
onTapped: if (! image.animated) Qt.openUrlExternally(fullSource)
|
||||
|
|
|
@ -7,10 +7,6 @@ import "../../utils.js" as Utils
|
|||
|
||||
VideoPlayer {
|
||||
id: video
|
||||
width: fullScreen ? implicitWidth : Math.min(
|
||||
mainColumn.width - eventContent.spacing * 2,
|
||||
theme.chat.message.videoWidth,
|
||||
)
|
||||
|
||||
onHoveredChanged:
|
||||
eventDelegate.hoveredMediaTypeUrl =
|
||||
|
|
|
@ -167,6 +167,16 @@ function thumbnailParametersFor(width, height) {
|
|||
}
|
||||
|
||||
|
||||
function fitSize(width, height, max) {
|
||||
if (width >= height) {
|
||||
let new_width = Math.min(width, max)
|
||||
return Qt.size(new_width, height / (width / new_width))
|
||||
}
|
||||
let new_height = Math.min(height, max)
|
||||
return Qt.size(width / (height / new_height), new_height)
|
||||
}
|
||||
|
||||
|
||||
function minutesBetween(date1, date2) {
|
||||
return ((date2 - date1) / 1000) / 60
|
||||
}
|
||||
|
|
|
@ -315,6 +315,7 @@ chat:
|
|||
string styleSheet:
|
||||
"* { white-space: pre-wrap }" +
|
||||
"a { color: " + link + " }" +
|
||||
"p { margin-top: 0 }" +
|
||||
|
||||
"code { font-family: " + fontFamily.mono + "; " +
|
||||
"color: " + code + " }" +
|
||||
|
@ -327,14 +328,17 @@ chat:
|
|||
"h5 { font-size: " + fontSize.small + "px }" +
|
||||
"h6 { font-size: " + fontSize.smaller + "px }" +
|
||||
|
||||
".sender { margin-bottom: " + spacing / 2 + " }" +
|
||||
".quote { color: " + quote + " }"
|
||||
|
||||
string styleInclude:
|
||||
'<style type"text/css">\n' + styleSheet + '\n</style>\n'
|
||||
|
||||
// TODO rename
|
||||
int fileMinWidth: 256
|
||||
int thumbnailWidth: 256
|
||||
int videoWidth: 512
|
||||
int audioWidth: 512
|
||||
int videoWidth: 640
|
||||
int audioWidth: 320
|
||||
|
||||
daybreak:
|
||||
color background: colors.strongBackground
|
||||
|
|
Loading…
Reference in New Issue
Block a user