Create a new module for the room info drawer QML.

Create a new module for the room info drawer QML. This also requires moving some QML to LibNeoChat common with other modules. Finally all QML in roominfo is modifed to not depend on app.
This commit is contained in:
James Graham
2025-05-17 14:27:38 +01:00
parent 495f7194ac
commit e7040a518a
19 changed files with 104 additions and 55 deletions

View File

@@ -41,12 +41,17 @@ target_sources(LibNeoChat PRIVATE
models/locationsmodel.cpp
models/roomlistmodel.cpp
models/stickermodel.cpp
models/userfiltermodel.cpp
models/userlistmodel.cpp
)
ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
URI org.kde.neochat.libneochat
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/libneochat
QML_FILES
qml/GroupChatDrawerHeader.qml
qml/LocationMapItem.qml
qml/SearchPage.qml
)
ecm_qt_declare_logging_category(LibNeoChat

View File

@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2022 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 "userfiltermodel.h"
#include "models/userlistmodel.h"
bool UserFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
Q_UNUSED(sourceParent);
if (!m_allowEmpty && m_filterText.length() < 1) {
return false;
}
return sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|| sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::UserIdRole).toString().contains(m_filterText, Qt::CaseInsensitive);
}
QString UserFilterModel::filterText() const
{
return m_filterText;
}
void UserFilterModel::setFilterText(const QString &filterText)
{
m_filterText = filterText;
Q_EMIT filterTextChanged();
invalidateFilter();
}
bool UserFilterModel::allowEmpty() const
{
return m_allowEmpty;
}
void UserFilterModel::setAllowEmpty(bool allowEmpty)
{
m_allowEmpty = allowEmpty;
Q_EMIT allowEmptyChanged();
}
#include "moc_userfiltermodel.cpp"

View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2022 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 <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class UserFilterModel
*
* This class creates a custom QSortFilterProxyModel for filtering a users by either
* display name or matrix ID. The filter can accept a full matrix id i.e. example:kde.org
* to separate between accounts on different servers with similar names.
*/
class UserFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief This property hold the text of the filter.
*
* The text is either a desired display name or matrix id.
*/
Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
Q_PROPERTY(bool allowEmpty READ allowEmpty WRITE setAllowEmpty NOTIFY allowEmptyChanged)
public:
/**
* @brief Custom filter function checking boith the display name and matrix ID.
*
* @note The filter cannot be modified and will always use the same filter properties.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
QString filterText() const;
void setFilterText(const QString &filterText);
bool allowEmpty() const;
void setAllowEmpty(bool allowEmpty);
Q_SIGNALS:
void filterTextChanged();
void allowEmptyChanged();
private:
QString m_filterText;
bool m_allowEmpty = false;
};

View File

@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.prison
import org.kde.neochat
ColumnLayout {
id: root
/**
* @brief The current room that user is viewing.
*/
required property NeoChatRoom room
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
KirigamiComponents.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.large
Layout.preferredHeight: Kirigami.Units.iconSizes.large
name: root.room ? root.room.displayName : ""
source: root.room ? root.room.avatarMediaUrl : ""
Rectangle {
visible: room.usesEncryption
color: Kirigami.Theme.backgroundColor
width: Kirigami.Units.gridUnit
height: Kirigami.Units.gridUnit
anchors {
bottom: parent.bottom
right: parent.right
}
radius: Math.round(width / 2)
Kirigami.Icon {
source: "channel-secure-symbolic"
anchors.fill: parent
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
Kirigami.Heading {
Layout.fillWidth: true
text: root.room ? root.room.displayName : i18n("No name")
textFormat: Text.PlainText
wrapMode: Text.Wrap
}
Kirigami.SelectableLabel {
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
textFormat: TextEdit.PlainText
visible: root.room && root.room.canonicalAlias
text: root.room && root.room.canonicalAlias ? root.room.canonicalAlias : ""
color: Kirigami.Theme.disabledTextColor
}
}
QQC2.AbstractButton {
Layout.preferredWidth: Kirigami.Units.iconSizes.large
Layout.preferredHeight: Kirigami.Units.iconSizes.large
Layout.rightMargin: Kirigami.Units.largeSpacing
contentItem: Barcode {
id: barcode
barcodeType: Barcode.QRCode
content: "https://matrix.to/#/" + root.room.id
}
onClicked: {
let map = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: barcode.content,
title: root.room ? root.room.displayName : "",
subtitle: root.room ? root.room.id : "",
avatarSource: root.room ? root.room.avatarMediaUrl : ""
});
map.open();
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: barcode.content
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
Kirigami.SelectableLabel {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
visible: text.length > 0
text: root.room && root.room.topic ? root.room.topic : ""
textFormat: TextEdit.MarkdownText
wrapMode: Text.Wrap
onLinkActivated: link => UrlHelper.openUrl(link)
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
applicationWindow().hoverLinkIndicator.text = hoveredLink;
} else {
applicationWindow().hoverLinkIndicator.text = "";
}
}
}

View File

@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtLocation
import QtPositioning
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
/** Location marker for any of the shared location maps. */
MapQuickItem {
id: root
required property real latitude
required property real longitude
required property string asset
required property var author
required property bool isLive
required property real heading
anchorPoint.x: sourceItem.width / 2
anchorPoint.y: sourceItem.height
coordinate: QtPositioning.coordinate(root.latitude, root.longitude)
autoFadeIn: false
sourceItem: Kirigami.Icon {
id: mainIcon
width: height
height: Kirigami.Units.iconSizes.huge
source: "gps"
isMask: true
color: root.isLive ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor
Kirigami.Icon {
anchors.centerIn: parent
anchors.verticalCenterOffset: -parent.height / 8
visible: root.asset === "m.pin"
width: height
height: parent.height / 3 + 1
source: "pin"
isMask: true
color: parent.color
}
KirigamiComponents.Avatar {
anchors.centerIn: parent
anchors.verticalCenterOffset: -parent.height / 8
visible: root.asset === "m.self"
width: height
height: parent.height / 3 + 1
name: root.author.displayName
source: root.author.avatarUrl
color: root.author.color
}
Kirigami.Icon {
id: headingIcon
source: "go-up-symbolic"
color: parent.color
visible: !isNaN(root.heading) && root.isLive
anchors.bottom: mainIcon.top
anchors.horizontalCenter: mainIcon.horizontalCenter
transform: Rotation {
origin.x: headingIcon.width / 2
origin.y: headingIcon.height + mainIcon.height / 2
angle: root.heading
}
}
}
}

View File

@@ -0,0 +1,211 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
/**
* @brief Component for a generic search page.
*
* This component provides a header with the search field and a ListView to visualise
* search results from the given model.
*/
Kirigami.ScrollablePage {
id: root
/**
* @brief Any additional controls after the search button.
*/
property alias headerTrailing: headerContent.children
/**
* @brief The model that provides the search results.
*
* The model needs to provide the following properties:
* - searchText
* - searching
* Where searchText is the text from the searchField and is used to match results
* and searching is true while the model is finding results.
*
* The model must also provide a search() function to start the search if
* it doesn't do so when the searchText is changed.
*/
property alias model: listView.model
/**
* @brief The number of delegates currently in the view.
*/
property alias count: listView.count
/**
* @brief The delegate to use to visualize the model data.
*/
property alias modelDelegate: listView.delegate
/**
* @brief The delegate to appear as the header of the list.
*/
property alias listHeaderDelegate: listView.header
/**
* @brief The delegate to appear as the footer of the list.
*/
property alias listFooterDelegate: listView.footer
/**
* @brief The placeholder text in the search field.
*/
property alias searchFieldPlaceholder: searchField.placeholderText
/**
* @brief The text to show when no search term has been entered.
*/
property alias noSearchPlaceholderMessage: noSearchMessage.text
/**
* @brief The text to show when no results have been found.
*/
property alias noResultPlaceholderMessage: noResultMessage.text
/**
* @brief The verticalLayoutDirection property of the internal ListView.
*/
property alias listVerticalLayoutDirection: listView.verticalLayoutDirection
/**
* @brief Set the visibility of the search button.
*/
property bool showSearchButton: true
/**
* @brief Message to be shown in a custom placeholder.
* The custom placeholder will be shown if the text is not empty
*/
property alias customPlaceholderText: customPlaceholder.text
/**
* @brief icon for the custom placeholder
*/
property string customPlaceholderIcon: ""
/**
* @brief Force the search field to be focussed.
*/
function focusSearch() {
searchField.forceActiveFocus();
}
/**
* @brief Force the search to be updated if the model has a valid search function.
*/
function updateSearch() {
searchTimer.restart();
}
header: QQC2.Control {
padding: Kirigami.Units.largeSpacing
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false
color: Kirigami.Theme.backgroundColor
Kirigami.Separator {
anchors {
left: parent.left
bottom: parent.bottom
right: parent.right
}
}
}
contentItem: RowLayout {
id: headerContent
spacing: Kirigami.Units.largeSpacing
Kirigami.SearchField {
id: searchField
focus: true
Layout.fillWidth: true
Keys.onEnterPressed: searchButton.clicked()
Keys.onReturnPressed: searchButton.clicked()
onTextChanged: {
searchTimer.restart();
if (model) {
model.searchText = text;
}
}
}
QQC2.Button {
id: searchButton
icon.name: "search"
display: QQC2.Button.IconOnly
visible: root.showSearchButton
text: i18nc("@action:button", "Search")
onClicked: {
if (typeof model.search === 'function') {
model.search();
}
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Timer {
id: searchTimer
interval: 500
running: true
onTriggered: if (typeof model.search === 'function') {
model.search();
}
}
}
}
ListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
section.property: "section"
Kirigami.PlaceholderMessage {
id: noSearchMessage
anchors.centerIn: parent
visible: searchField.text.length === 0 && listView.count === 0 && customPlaceholder.text.length === 0
}
Kirigami.PlaceholderMessage {
id: noResultMessage
anchors.centerIn: parent
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching && customPlaceholder.text.length === 0
}
Kirigami.PlaceholderMessage {
id: customPlaceholder
anchors.centerIn: parent
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching && text.length > 0
icon.name: root.customPlaceholderIcon
}
Kirigami.LoadingPlaceholder {
anchors.centerIn: parent
visible: searchField.text.length > 0 && listView.count === 0 && root.model.searching && customPlaceholder.text.length === 0
}
Keys.onUpPressed: {
if (listView.currentIndex > 0) {
listView.decrementCurrentIndex();
} else {
listView.currentIndex = -1; // This is so the list view doesn't appear to have two selected items
listView.headerItem.forceActiveFocus(Qt.TabFocusReason);
}
}
}
}