Add support for copying & deleting multiple messages at once

BUG: 496458
This commit is contained in:
Azhar Momin
2026-02-11 18:30:09 +05:30
committed by Joshua Goins
parent 0f634ff795
commit f5d726989f
10 changed files with 357 additions and 15 deletions

View File

@@ -407,6 +407,13 @@ KirigamiComponents.ConvergentContextMenu {
onTriggered: pinned ? root.room.unpinEvent(root.eventId) : root.room.pinEvent(root.eventId)
}
Kirigami.Action {
visible: root.messageComponentType !== MessageComponentType.Other
text: root.room.selectedMessageCount > 0 && root.room.isMessageSelected(root.eventId) ? i18nc("@action:inmenu", "Deselect Message") : i18nc("@action:inmenu", "Select Message")
icon.name: "edit-select-all-symbolic"
onTriggered: root.room.toggleMessageSelection(root.eventId)
}
Kirigami.Action {
separator: true
visible: viewSourceAction.visible

View File

@@ -2,6 +2,8 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-only
pragma ComponentBehavior: Bound
import QtQuick
import Qt.labs.qmlmodels
@@ -16,6 +18,11 @@ DelegateChooser {
*/
required property NeoChatRoom room
/**
* @brief Whether to show selection controls for message delegate.
*/
property bool showSelectionControl: false
role: "delegateType"
DelegateChoice {
@@ -25,7 +32,9 @@ DelegateChooser {
DelegateChoice {
roleValue: DelegateType.Message
delegate: MessageDelegate {}
delegate: MessageDelegate {
showSelectionControl: root.showSelectionControl
}
}
DelegateChoice {

View File

@@ -5,6 +5,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
@@ -109,6 +110,16 @@ MessageDelegateBase {
*/
property bool showHighlight: root.isHighlighted || isTemporaryHighlighted
/**
* @brief Whether the message is selected.
*/
property bool selected: root.room.selectedMessageCount > 0 && room.isMessageSelected(eventId)
/**
* @brief Whether to show selection controls for this message.
*/
property bool showSelectionControl: false
Message.room: root.room
Message.timeline: root.ListView.view
Message.contentModel: root.contentModel
@@ -120,6 +131,7 @@ MessageDelegateBase {
enableAvatars: NeoChatConfig?.showAvatarInTimeline ?? false
compactMode: NeoChatConfig?.compactLayout ?? false
showLocalMessagesOnRight: NeoChatConfig.showLocalMessagesOnRight
showSelection: root.showSelectionControl && room.selectedMessageCount > 0
contentItem: Bubble {
id: bubble
@@ -230,6 +242,20 @@ MessageDelegateBase {
author: root.author
}
selectionComponent: RowLayout {
spacing: Kirigami.Units.smallSpacing
implicitHeight: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
QQC2.CheckBox {
checked: root.selected
onClicked: root.room.toggleMessageSelection(root.eventId)
}
Kirigami.Separator {
Layout.fillHeight: true
}
}
QtObject {
id: _private

View File

@@ -213,6 +213,7 @@ QQC2.ScrollView {
model: root.messageFilterModel
delegate: EventDelegate {
room: _private.room
showSelectionControl: true
}
ColumnLayout {

View File

@@ -138,7 +138,10 @@ void MessageDelegateBase::setPercentageValues(bool fillWidth)
void MessageDelegateBase::setContentPadding()
{
m_contentSizeHelper.setLeftPadding(m_sizeHelper.leftX() + (leaveAvatarSpace() ? m_avatarSize + m_spacing : 0));
qreal selectionOffset = (m_showSelection && m_selectionItem) ? m_selectionItem->implicitWidth() + (m_spacing * 2) : 0;
qreal avatarOffset = (leaveAvatarSpace() ? m_avatarSize + m_spacing : 0);
m_contentSizeHelper.setLeftPadding(m_sizeHelper.leftX() + selectionOffset + avatarOffset);
m_contentSizeHelper.setRightPadding(m_sizeHelper.rightPadding());
}
@@ -539,6 +542,77 @@ void MessageDelegateBase::updateQuickAction()
m_quickActionComponent->create(*quickActionIncubator, qmlContext(m_quickActionComponent));
}
QQmlComponent *MessageDelegateBase::selectionComponent() const
{
return m_selectionComponent;
}
void MessageDelegateBase::setSelectionComponent(QQmlComponent *selectionComponent)
{
if (selectionComponent == m_selectionComponent) {
return;
}
m_selectionComponent = selectionComponent;
Q_EMIT selectionComponentChanged();
updateSelection();
}
bool MessageDelegateBase::showSelection() const
{
return m_showSelection;
}
void MessageDelegateBase::setShowSelection(bool showSelection)
{
if (showSelection == m_showSelection) {
return;
}
m_showSelection = showSelection;
Q_EMIT showSelectionChanged();
updateSelection();
}
void MessageDelegateBase::updateSelection()
{
if (m_selectionComponent && showSelection() && !m_selectionItem && !m_selectionIncubating) {
const auto selectionIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
const auto selectionObject = qobject_cast<QQuickItem *>(incubator->object());
if (selectionObject) {
// The setting may have changed during the incubation period.
if (showSelection()) {
m_selectionItem = selectionObject;
} else {
cleanupItem(selectionObject);
}
setContentPadding();
markAsDirty();
}
m_selectionIncubating = false;
// We can't cleanup the incubator in the completedCallback otherwise
// we use after free when we return to the status changed function
// of that incubator
QTimer::singleShot(0, this, [this, incubator]() {
cleanupIncubator(incubator);
});
},
m_errorCallback);
m_activeIncubators.push_back(selectionIncubator);
m_selectionComponent->create(*selectionIncubator, qmlContext(m_selectionComponent));
m_selectionIncubating = true;
} else if (!showSelection() && m_selectionItem) {
cleanupItem(m_selectionItem);
setContentPadding();
markAsDirty();
}
}
bool MessageDelegateBase::showLocalMessagesOnRight() const
{
return m_showLocalMessagesOnRight;
@@ -623,10 +697,17 @@ void MessageDelegateBase::resizeContent()
nextY += m_sectionItem->implicitHeight() + m_spacing;
}
qreal yAdd = 0.0;
if (m_showSelection && m_selectionItem) {
m_selectionItem->setPosition(QPointF(m_sizeHelper.leftX(), nextY));
m_selectionItem->setSize(QSizeF(m_selectionItem->implicitWidth(), m_selectionItem->implicitHeight()));
yAdd = m_selectionItem->implicitHeight();
}
if (showAvatar() && m_avatarItem) {
m_avatarItem->setPosition(QPointF(m_sizeHelper.leftX(), nextY));
m_avatarItem->setPosition(
QPointF(m_showSelection && m_selectionItem ? m_sizeHelper.leftX() + m_selectionItem->implicitWidth() + (m_spacing * 2) : m_sizeHelper.leftX(),
nextY));
m_avatarItem->setSize(QSizeF(m_avatarItem->implicitWidth(), m_avatarItem->implicitHeight()));
yAdd = m_avatarItem->implicitWidth();
yAdd = std::max(yAdd, m_avatarItem->implicitHeight());
}
if (m_contentItem) {
const auto contentItemWidth =

View File

@@ -101,6 +101,16 @@ class MessageDelegateBase : public TimelineDelegate
*/
Q_PROPERTY(QQmlComponent *quickActionComponent READ quickActionComponent WRITE setQuickActionComponent NOTIFY quickActionComponentChanged FINAL)
/**
* @brief The component to use to visualize message selection.
*/
Q_PROPERTY(QQmlComponent *selectionComponent READ selectionComponent WRITE setSelectionComponent NOTIFY selectionComponentChanged FINAL)
/**
* @brief Whether to show the selection component.
*/
Q_PROPERTY(bool showSelection READ showSelection WRITE setShowSelection NOTIFY showSelectionChanged FINAL REQUIRED)
/**
* @brief Whether to use the compact mode appearance.
*/
@@ -161,6 +171,11 @@ public:
QQmlComponent *quickActionComponent() const;
void setQuickActionComponent(QQmlComponent *quickActionComponent);
QQmlComponent *selectionComponent() const;
void setSelectionComponent(QQmlComponent *selectionComponent);
bool showSelection() const;
void setShowSelection(bool showSelection);
bool showLocalMessagesOnRight() const;
void setShowLocalMessagesOnRight(bool showLocalMessagesOnRight);
@@ -182,6 +197,8 @@ Q_SIGNALS:
void showReadMarkersChanged();
void compactBackgroundComponentChanged();
void quickActionComponentChanged();
void selectionComponentChanged();
void showSelectionChanged();
void compactModeChanged();
void showLocalMessagesOnRightChanged();
void isTemporaryHighlightedChanged();
@@ -227,6 +244,12 @@ private:
bool m_quickActionIncubating = false;
QPointer<QQuickItem> m_quickActionItem;
QPointer<QQmlComponent> m_selectionComponent;
bool m_selectionIncubating = false;
QPointer<QQuickItem> m_selectionItem;
bool m_showSelection = false;
void updateSelection();
bool m_showLocalMessagesOnRight = true;
bool m_hovered = false;