MediaSizeHelper
Create a media size helper and use it to force video and images to be the correct size even in replies.
This commit is contained in:
@@ -132,6 +132,8 @@ add_library(neochat STATIC
|
||||
jobs/neochatdeletedevicejob.h
|
||||
jobs/neochatchangepasswordjob.cpp
|
||||
jobs/neochatchangepasswordjob.h
|
||||
mediasizehelper.cpp
|
||||
mediasizehelper.h
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
#include "logger.h"
|
||||
#include "login.h"
|
||||
#include "matriximageprovider.h"
|
||||
#include "mediasizehelper.h"
|
||||
#include "models/accountemoticonmodel.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "models/devicesmodel.h"
|
||||
@@ -278,6 +279,7 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<AccountEmoticonModel>("org.kde.neochat", 1, 0, "AccountEmoticonModel");
|
||||
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
|
||||
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
|
||||
qmlRegisterType<MediaSizeHelper>("org.kde.neochat", 1, 0, "MediaSizeHelper");
|
||||
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"_ls);
|
||||
|
||||
163
src/mediasizehelper.cpp
Normal file
163
src/mediasizehelper.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "mediasizehelper.h"
|
||||
|
||||
#include "neochatconfig.h"
|
||||
|
||||
MediaSizeHelper::MediaSizeHelper(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::contentMaxWidth() const
|
||||
{
|
||||
return m_contentMaxWidth;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setContentMaxWidth(qreal contentMaxWidth)
|
||||
{
|
||||
if (contentMaxWidth < 0.0 || qFuzzyCompare(contentMaxWidth, 0.0)) {
|
||||
m_contentMaxWidth = -1.0;
|
||||
Q_EMIT contentMaxWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(contentMaxWidth, m_contentMaxWidth)) {
|
||||
return;
|
||||
}
|
||||
m_contentMaxWidth = contentMaxWidth;
|
||||
Q_EMIT contentMaxWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::contentMaxHeight() const
|
||||
{
|
||||
return m_contentMaxHeight;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setContentMaxHeight(qreal contentMaxHeight)
|
||||
{
|
||||
if (contentMaxHeight < 0.0 || qFuzzyCompare(contentMaxHeight, 0.0)) {
|
||||
m_contentMaxHeight = -1.0;
|
||||
Q_EMIT contentMaxHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(contentMaxHeight, m_contentMaxHeight)) {
|
||||
return;
|
||||
}
|
||||
m_contentMaxHeight = contentMaxHeight;
|
||||
Q_EMIT contentMaxHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::mediaWidth() const
|
||||
{
|
||||
return m_mediaWidth;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setMediaWidth(qreal mediaWidth)
|
||||
{
|
||||
if (mediaWidth < 0.0 || qFuzzyCompare(mediaWidth, 0.0)) {
|
||||
m_mediaWidth = -1.0;
|
||||
Q_EMIT mediaWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(mediaWidth, m_mediaWidth)) {
|
||||
return;
|
||||
}
|
||||
m_mediaWidth = mediaWidth;
|
||||
Q_EMIT mediaWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::mediaHeight() const
|
||||
{
|
||||
return m_mediaHeight;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setMediaHeight(qreal mediaHeight)
|
||||
{
|
||||
if (mediaHeight < 0.0 || qFuzzyCompare(mediaHeight, 0.0)) {
|
||||
m_mediaHeight = -1.0;
|
||||
Q_EMIT mediaHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(mediaHeight, m_mediaHeight)) {
|
||||
return;
|
||||
}
|
||||
m_mediaHeight = mediaHeight;
|
||||
Q_EMIT mediaHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::resolvedMediaWidth() const
|
||||
{
|
||||
if (m_mediaWidth > 0.0) {
|
||||
return m_mediaWidth;
|
||||
}
|
||||
return widthLimit();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::resolvedMediaHeight() const
|
||||
{
|
||||
if (m_mediaHeight > 0.0) {
|
||||
return m_mediaHeight;
|
||||
}
|
||||
return widthLimit() / 16.0 * 9.0;
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::aspectRatio() const
|
||||
{
|
||||
return resolvedMediaWidth() / resolvedMediaHeight();
|
||||
}
|
||||
|
||||
bool MediaSizeHelper::limitWidth() const
|
||||
{
|
||||
// If actual data isn't available we'll be using a placeholder that is width
|
||||
// limited so return true.
|
||||
if (m_mediaWidth < 0.0 || m_mediaHeight < 0.0) {
|
||||
return true;
|
||||
}
|
||||
return m_mediaWidth >= m_mediaHeight;
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::widthLimit() const
|
||||
{
|
||||
if (m_contentMaxWidth < 0.0) {
|
||||
return NeoChatConfig::self()->mediaMaxWidth();
|
||||
}
|
||||
return std::min(m_contentMaxWidth, qreal(NeoChatConfig::self()->mediaMaxWidth()));
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::heightLimit() const
|
||||
{
|
||||
if (m_contentMaxHeight < 0.0) {
|
||||
return NeoChatConfig::self()->mediaMaxHeight();
|
||||
}
|
||||
return std::min(m_contentMaxHeight, qreal(NeoChatConfig::self()->mediaMaxHeight()));
|
||||
}
|
||||
|
||||
QSize MediaSizeHelper::currentSize() const
|
||||
{
|
||||
if (limitWidth()) {
|
||||
qreal width = std::min(widthLimit(), resolvedMediaWidth());
|
||||
qreal height = width / aspectRatio();
|
||||
if (height > heightLimit()) {
|
||||
return QSize(qRound(heightLimit() * aspectRatio()), qRound(heightLimit()));
|
||||
}
|
||||
return QSize(qRound(width), qRound(height));
|
||||
} else {
|
||||
qreal height = std::min(heightLimit(), resolvedMediaHeight());
|
||||
qreal width = height * aspectRatio();
|
||||
if (width > widthLimit()) {
|
||||
return QSize(qRound(widthLimit()), qRound(widthLimit() / aspectRatio()));
|
||||
}
|
||||
return QSize(qRound(width), qRound(height));
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_mediasizehelper.cpp"
|
||||
103
src/mediasizehelper.h
Normal file
103
src/mediasizehelper.h
Normal file
@@ -0,0 +1,103 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QSize>
|
||||
|
||||
/**
|
||||
* @class MediaSizeHelper
|
||||
*
|
||||
* A class to help calculate the current width of a media item within a chat delegate.
|
||||
*
|
||||
* The only realistic way to guarantee that a media item (e.g. an image or video)
|
||||
* is the correct size in QML is to calculate the size manually.
|
||||
*
|
||||
* The rules for this component work as follows:
|
||||
* - The output will always try to keep the media size if no limits are breached.
|
||||
* - If no media width is set, the current size will be a placeholder at a 16:9 ratio
|
||||
* calcualated from either the configured max width or the contentMaxWidth, whichever
|
||||
* is smaller (if the contentMaxWidth isn't set, the configured max width is used).
|
||||
* - The aspect ratio of the media will always be maintained if set (otherwise 16:9).
|
||||
* - The current size will never be larger than any of the limits in either direction.
|
||||
* - If any limit is breached the image size will be reduced while maintaining aspect
|
||||
* ration, i.e. no stretching or squashing. This can mean that the width or height
|
||||
* is reduced even if that parameter doesn't breach the limit itself.
|
||||
*/
|
||||
class MediaSizeHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The maximum width (in px) the media can be.
|
||||
*
|
||||
* This is the upper limit placed upon the media by the delegate.
|
||||
*/
|
||||
Q_PROPERTY(qreal contentMaxWidth READ contentMaxWidth WRITE setContentMaxWidth NOTIFY contentMaxWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The maximum height (in px) the media can be.
|
||||
*
|
||||
* This is the upper limit placed upon the media by the delegate.
|
||||
*/
|
||||
Q_PROPERTY(qreal contentMaxHeight READ contentMaxHeight WRITE setContentMaxHeight NOTIFY contentMaxHeightChanged)
|
||||
|
||||
/**
|
||||
* @brief The base width (in px) of the media.
|
||||
*/
|
||||
Q_PROPERTY(qreal mediaWidth READ mediaWidth WRITE setMediaWidth NOTIFY mediaWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The base height (in px) of the media.
|
||||
*/
|
||||
Q_PROPERTY(qreal mediaHeight READ mediaHeight WRITE setMediaHeight NOTIFY mediaHeightChanged)
|
||||
|
||||
/**
|
||||
* @brief The size (in px) of the component based on the current input.
|
||||
*
|
||||
* Will always try to return a value even if some of the inputs are not set to
|
||||
* account for being called before the parameters are intialised. For any parameters
|
||||
* not set these will just be left out of the calcs.
|
||||
*
|
||||
* If no input values are provided a default placeholder value will be returned.
|
||||
*/
|
||||
Q_PROPERTY(QSize currentSize READ currentSize NOTIFY currentSizeChanged)
|
||||
|
||||
public:
|
||||
explicit MediaSizeHelper(QObject *parent = nullptr);
|
||||
|
||||
qreal contentMaxWidth() const;
|
||||
void setContentMaxWidth(qreal contentMaxWidth);
|
||||
|
||||
qreal contentMaxHeight() const;
|
||||
void setContentMaxHeight(qreal contentMaxHeight);
|
||||
|
||||
qreal mediaWidth() const;
|
||||
void setMediaWidth(qreal mediaWidth);
|
||||
|
||||
qreal mediaHeight() const;
|
||||
void setMediaHeight(qreal mediaHeight);
|
||||
|
||||
QSize currentSize() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void contentMaxWidthChanged();
|
||||
void contentMaxHeightChanged();
|
||||
void mediaWidthChanged();
|
||||
void mediaHeightChanged();
|
||||
void currentSizeChanged();
|
||||
|
||||
private:
|
||||
qreal m_contentMaxWidth = -1.0;
|
||||
qreal m_contentMaxHeight = -1.0;
|
||||
qreal m_mediaWidth = -1.0;
|
||||
qreal m_mediaHeight = -1.0;
|
||||
|
||||
qreal resolvedMediaWidth() const;
|
||||
qreal resolvedMediaHeight() const;
|
||||
qreal aspectRatio() const;
|
||||
bool limitWidth() const;
|
||||
qreal widthLimit() const;
|
||||
qreal heightLimit() const;
|
||||
};
|
||||
@@ -107,6 +107,14 @@
|
||||
<label>Show Fancy Effects</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="MediaMaxWidth" type="int">
|
||||
<label>The maximum width any media item in the timeline can be.</label>
|
||||
<default>540</default>
|
||||
</entry>
|
||||
<entry name="MediaMaxHeight" type="int">
|
||||
<label>The maximum height any media item in the timeline can be.</label>
|
||||
<default>540</default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="RoomDrawer">
|
||||
<entry name="ShowAvatarInRoomDrawer" type="bool">
|
||||
|
||||
@@ -57,48 +57,8 @@ TimelineContainer {
|
||||
|
||||
innerObject: Item {
|
||||
id: imageContainer
|
||||
|
||||
property var imageWidth: {
|
||||
if (root.mediaInfo.width > 0) {
|
||||
return root.mediaInfo.width;
|
||||
} else {
|
||||
return root.contentMaxWidth;
|
||||
}
|
||||
}
|
||||
property var imageHeight: {
|
||||
if (root.mediaInfo.height > 0) {
|
||||
return root.mediaInfo.height;
|
||||
} else {
|
||||
// Default to a 16:9 placeholder
|
||||
return root.contentMaxWidth / 16 * 9;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var aspectRatio: imageWidth / imageHeight
|
||||
/**
|
||||
* Whether the image should be limited by height or width.
|
||||
* We need to prevent excessively tall as well as excessively wide media.
|
||||
*
|
||||
* @note In the case of a tie the media is width limited.
|
||||
*/
|
||||
readonly property bool limitWidth: imageWidth >= imageHeight
|
||||
|
||||
readonly property size maxSize: {
|
||||
if (limitWidth) {
|
||||
let width = Math.min(root.contentMaxWidth, root.maxWidth);
|
||||
let height = width / aspectRatio;
|
||||
return Qt.size(width, height);
|
||||
} else {
|
||||
let height = Math.min(root.maxHeight, root.contentMaxWidth / aspectRatio);
|
||||
let width = height * aspectRatio;
|
||||
return Qt.size(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
Layout.maximumWidth: maxSize.width
|
||||
Layout.maximumHeight: maxSize.height
|
||||
Layout.preferredWidth: imageWidth
|
||||
Layout.preferredHeight: imageHeight
|
||||
Layout.preferredWidth: mediaSizeHelper.currentSize.width
|
||||
Layout.preferredHeight: mediaSizeHelper.currentSize.height
|
||||
|
||||
property var imageItem: root.mediaInfo.animated ? animatedImageLoader.item : imageLoader.item
|
||||
|
||||
@@ -192,5 +152,12 @@ TimelineContainer {
|
||||
if (UrlHelper.openUrl(root.progressInfo.localPath)) return;
|
||||
if (UrlHelper.openUrl(root.progressInfo.localDir)) return;
|
||||
}
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.contentMaxWidth
|
||||
mediaWidth: root.mediaInfo.width
|
||||
mediaHeight: root.mediaInfo.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ Item {
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
required property real contentMaxWidth
|
||||
|
||||
/**
|
||||
* @brief The reply has been clicked.
|
||||
*/
|
||||
@@ -167,27 +169,17 @@ Item {
|
||||
id: imageComponent
|
||||
Image {
|
||||
id: image
|
||||
|
||||
property var imageWidth: {
|
||||
if (root.mediaInfo.width > 0) {
|
||||
return root.mediaInfo.width;
|
||||
} else {
|
||||
return sourceSize.width;
|
||||
}
|
||||
}
|
||||
property var imageHeight: {
|
||||
if (root.mediaInfo.height > 0) {
|
||||
return root.mediaInfo.height;
|
||||
} else {
|
||||
return sourceSize.height;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var aspectRatio: imageWidth / imageHeight
|
||||
|
||||
height: width / aspectRatio
|
||||
width: mediaSizeHelper.currentSize.width
|
||||
height: mediaSizeHelper.currentSize.height
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: root.mediaInfo.source
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.contentMaxWidth - verticalBorder.width - mainLayout.columnSpacing
|
||||
mediaWidth: root.mediaInfo.width
|
||||
mediaHeight: root.mediaInfo.height
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
|
||||
@@ -487,6 +487,7 @@ ColumnLayout {
|
||||
type: root.reply.type
|
||||
display: root.reply.display
|
||||
mediaInfo: root.replyMediaInfo
|
||||
contentMaxWidth: bubbleSizeHelper.currentWidth
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -69,53 +69,8 @@ TimelineContainer {
|
||||
|
||||
innerObject: Video {
|
||||
id: vid
|
||||
|
||||
property var videoWidth: {
|
||||
if (root.mediaInfo.width > 0) {
|
||||
return root.mediaInfo.width;
|
||||
} else if (metaData.resolution && metaData.resolution.width) {
|
||||
return metaData.resolution.width;
|
||||
} else {
|
||||
return root.contentMaxWidth;
|
||||
}
|
||||
}
|
||||
property var videoHeight: {
|
||||
if (root.mediaInfo.height > 0) {
|
||||
return root.mediaInfo.height;
|
||||
} else if (metaData.resolution && metaData.resolution.height) {
|
||||
return metaData.resolution.height;
|
||||
} else {
|
||||
// Default to a 16:9 placeholder
|
||||
return root.contentMaxWidth / 16 * 9;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var aspectRatio: videoWidth / videoHeight
|
||||
/**
|
||||
* Whether the video should be limited by height or width.
|
||||
* We need to prevent excessively tall as well as excessively wide media.
|
||||
*
|
||||
* @note In the case of a tie the media is width limited.
|
||||
*/
|
||||
readonly property bool limitWidth: videoWidth >= videoHeight
|
||||
|
||||
readonly property size maxSize: {
|
||||
if (limitWidth) {
|
||||
let width = Math.min(root.contentMaxWidth, root.maxWidth);
|
||||
let height = width / aspectRatio;
|
||||
return Qt.size(width, height);
|
||||
} else {
|
||||
let height = Math.min(root.maxHeight, root.contentMaxWidth / aspectRatio);
|
||||
let width = height * aspectRatio;
|
||||
return Qt.size(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
Layout.maximumWidth: maxSize.width
|
||||
Layout.maximumHeight: maxSize.height
|
||||
|
||||
Layout.preferredWidth: videoWidth
|
||||
Layout.preferredHeight: videoHeight
|
||||
Layout.preferredWidth: mediaSizeHelper.currentSize.width
|
||||
Layout.preferredHeight: mediaSizeHelper.currentSize.height
|
||||
|
||||
fillMode: VideoOutput.PreserveAspectFit
|
||||
@QTMULTIMEDIA_VIDEO_FLUSHMODE@
|
||||
@@ -383,6 +338,13 @@ TimelineContainer {
|
||||
root.downloadAndPlay()
|
||||
}
|
||||
}
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.contentMaxWidth
|
||||
mediaWidth: root.mediaInfo.width
|
||||
mediaHeight: root.mediaInfo.height
|
||||
}
|
||||
}
|
||||
|
||||
function downloadAndPlay() {
|
||||
|
||||
Reference in New Issue
Block a user