From 11343e6bdf49e4045fe5dc1cc9bd389e96b2af2d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 9 May 2023 08:11:47 +0000 Subject: [PATCH] Improve spaces support --- src/models/roomlistmodel.cpp | 8 + src/models/roomlistmodel.h | 1 + src/models/sortfilterspacelistmodel.cpp | 13 +- src/models/sortfilterspacelistmodel.h | 7 + src/qml/Component/AvatarTabButton.qml | 190 +++++++++++++++++ src/qml/Page/RoomList/Page.qml | 263 ++++++++++++------------ src/qml/Page/RoomList/SpaceDrawer.qml | 115 +++++++++++ src/res.qrc | 4 +- src/spacehierarchycache.cpp | 18 +- src/spacehierarchycache.h | 5 + 10 files changed, 490 insertions(+), 134 deletions(-) create mode 100644 src/qml/Component/AvatarTabButton.qml create mode 100644 src/qml/Page/RoomList/SpaceDrawer.qml diff --git a/src/models/roomlistmodel.cpp b/src/models/roomlistmodel.cpp index f82d2c33f..68f7e353c 100644 --- a/src/models/roomlistmodel.cpp +++ b/src/models/roomlistmodel.cpp @@ -7,6 +7,7 @@ #include "neochatconfig.h" #include "neochatroom.h" #include "roommanager.h" +#include "spacehierarchycache.h" #include "user.h" #include @@ -65,6 +66,9 @@ RoomListModel::RoomListModel(QObject *parent) qGuiApp->setBadgeNumber(m_notificationCount); #endif // QT_VERSION_CHECK(6, 6, 0) }); + connect(&SpaceHierarchyCache::instance(), &SpaceHierarchyCache::spaceHierarchyChanged, this, [this]() { + Q_EMIT dataChanged(index(0, 0), index(rowCount(), 0), {IsChildSpaceRole}); + }); } RoomListModel::~RoomListModel() = default; @@ -412,6 +416,9 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const if (role == IsSpaceRole) { return room->isSpace(); } + if (role == IsChildSpaceRole) { + return SpaceHierarchyCache::instance().isChildSpace(room->id()); + } return QVariant(); } @@ -447,6 +454,7 @@ QHash RoomListModel::roleNames() const roles[SubtitleTextRole] = "subtitleText"; roles[IsSpaceRole] = "isSpace"; roles[IdRole] = "id"; + roles[IsChildSpaceRole] = "isChildSpace"; return roles; } diff --git a/src/models/roomlistmodel.h b/src/models/roomlistmodel.h index 6ee1f1fd4..7b22856eb 100644 --- a/src/models/roomlistmodel.h +++ b/src/models/roomlistmodel.h @@ -76,6 +76,7 @@ public: AvatarImageRole, /**< The room avatar as an image. */ IdRole, /**< The room matrix ID. */ IsSpaceRole, /**< Whether the room is a space. */ + IsChildSpaceRole, /**< Whether this space is a child of a different space. */ }; Q_ENUM(EventRoles) diff --git a/src/models/sortfilterspacelistmodel.cpp b/src/models/sortfilterspacelistmodel.cpp index 33ebf2695..63d782896 100644 --- a/src/models/sortfilterspacelistmodel.cpp +++ b/src/models/sortfilterspacelistmodel.cpp @@ -11,13 +11,24 @@ SortFilterSpaceListModel::SortFilterSpaceListModel(QObject *parent) setSortRole(RoomListModel::IdRole); sort(0); invalidateFilter(); + connect(this, &QAbstractProxyModel::sourceModelChanged, this, [this]() { + connect(sourceModel(), &QAbstractListModel::dataChanged, this, [this](const QModelIndex &, const QModelIndex &, QVector roles) { + if (roles.contains(RoomListModel::IsChildSpaceRole)) { + invalidate(); + } + countChanged(); + }); + invalidate(); + Q_EMIT countChanged(); + }); } bool SortFilterSpaceListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { Q_UNUSED(source_parent); return sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() - && sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded"; + && sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded" + && !sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsChildSpaceRole).toBool(); } bool SortFilterSpaceListModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const diff --git a/src/models/sortfilterspacelistmodel.h b/src/models/sortfilterspacelistmodel.h index f3fba400b..40c611876 100644 --- a/src/models/sortfilterspacelistmodel.h +++ b/src/models/sortfilterspacelistmodel.h @@ -16,10 +16,17 @@ class SortFilterSpaceListModel : public QSortFilterProxyModel { Q_OBJECT + /** + * @brief The number of spaces in the model. + */ + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: explicit SortFilterSpaceListModel(QObject *parent = nullptr); +Q_SIGNALS: + void countChanged(); + protected: /** * @brief Returns true if the value of source_left is less than source_right. diff --git a/src/qml/Component/AvatarTabButton.qml b/src/qml/Component/AvatarTabButton.qml new file mode 100644 index 000000000..7ef72e707 --- /dev/null +++ b/src/qml/Component/AvatarTabButton.qml @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2021 Devin Lin +// SPDX-FileCopyrightText: 2021 Noah Davis +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Templates 2.15 as T +import org.kde.kirigami 2.19 as Kirigami + +T.TabButton { + id: control + + /** + * @brief This property specifies the index of this tab within the tab bar. + */ + readonly property int tabIndex: { + let tabIdx = 0 + for (let i = 0; i < parent.children.length; ++i) { + if (parent.children[i] === this) { + return tabIdx + } + // Checking for AbstractButtons because any AbstractButton can act as a tab + if (parent.children[i] instanceof T.AbstractButton) { + ++tabIdx + } + } + return -1 + } + + /** + * @brief This property sets whether the icon colors should be masked with a single color. + * + * default: ``true`` + * + * @since KDE Frameworks 5.96 + */ + property bool recolorIcon: true + + property color foregroundColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.85) + property color highlightForegroundColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.85) + property color highlightBarColor: Kirigami.Theme.highlightColor + + property color pressedColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.3) + property color hoverSelectColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.2) + property color checkedBorderColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.7) + property color pressedBorderColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.9) + + property url source + property string name + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + display: T.AbstractButton.TextUnderIcon + + Kirigami.Theme.colorSet: Kirigami.Theme.Window + Kirigami.Theme.inherit: false + + // not using the hover handler built into control, since it seems to misbehave and + // permanently report hovered after a touch event + HoverHandler { + id: hoverHandler + } + + padding: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + + icon.height: control.display === T.AbstractButton.TextBesideIcon ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium + icon.width: control.display === T.AbstractButton.TextBesideIcon ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium + icon.color: control.checked ? control.highlightForegroundColor : control.foregroundColor + + background: Rectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.Button + Kirigami.Theme.inherit: false + + implicitHeight: (control.display === T.AbstractButton.TextBesideIcon) ? 0 : (Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2) + + color: "transparent" + + Rectangle { + width: parent.width - Kirigami.Units.largeSpacing + height: parent.height - Kirigami.Units.largeSpacing + anchors.centerIn: parent + + radius: Kirigami.Units.smallSpacing + color: control.down ? pressedColor : (control.checked || hoverHandler.hovered ? hoverSelectColor : "transparent") + + border.color: control.checked ? checkedBorderColor : (control.down ? pressedBorderColor : color) + border.width: 1 + + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + Behavior on border.color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + } + } + + contentItem: GridLayout { + id: gridLayout + columnSpacing: 0 + rowSpacing: (label.visible && label.lineCount > 1) ? 0 : control.spacing + + // if this is a row or a column + columns: control.display !== T.AbstractButton.TextBesideIcon ? 1 : 2 + + property real verticalMargins: (control.display === T.AbstractButton.TextBesideIcon) ? Kirigami.Units.largeSpacing : 0 + + Kirigami.Avatar { + id: icon + source: control.source + name: control.name + Layout.topMargin: gridLayout.verticalMargins + Layout.bottomMargin: gridLayout.verticalMargins + Layout.leftMargin: (control.display === T.AbstractButton.TextBesideIcon) ? Kirigami.Units.gridUnit : 0 + Layout.rightMargin: (control.display === T.AbstractButton.TextBesideIcon) ? Kirigami.Units.gridUnit : 0 + + Layout.alignment: { + if (control.display === T.AbstractButton.TextBesideIcon) { + // row layout + return Qt.AlignVCenter | Qt.AlignRight; + } else { + // column layout + return Qt.AlignHCenter | ((!label.visible || label.lineCount > 1) ? Qt.AlignVCenter : Qt.AlignBottom); + } + } + Layout.preferredWidth: Kirigami.Units.iconSizes.medium + Layout.preferredHeight: Kirigami.Units.iconSizes.medium + } + QQC2.Label { + id: label + Kirigami.MnemonicData.enabled: control.enabled && control.visible + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.MenuItem + Kirigami.MnemonicData.label: control.text + + text: Kirigami.MnemonicData.richTextLabel + horizontalAlignment: (control.display === T.AbstractButton.TextBesideIcon) ? Text.AlignLeft : Text.AlignHCenter + + visible: control.display !== T.AbstractButton.IconOnly + wrapMode: Text.Wrap + elide: Text.ElideMiddle + color: control.checked ? control.highlightForegroundColor : control.foregroundColor + + font.bold: control.checked + font.family: Kirigami.Theme.smallFont.family + font.pointSize: { + if (control.display === T.AbstractButton.TextBesideIcon) { + // row layout + return Kirigami.Theme.defaultFont.pointSize; + } else { + // column layout + return icon.visible ? Kirigami.Theme.smallFont.pointSize : Kirigami.Theme.defaultFont.pointSize * 1.20; // 1.20 is equivalent to level 2 heading + } + } + + Behavior on color { ColorAnimation {} } + Behavior on opacity { NumberAnimation {} } + + Layout.topMargin: gridLayout.verticalMargins + Layout.bottomMargin: gridLayout.verticalMargins + + Layout.alignment: { + if (control.display === T.AbstractButton.TextBesideIcon) { + // row layout + return Qt.AlignVCenter | Qt.AlignLeft; + } else { + // column layout + return icon.visible ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter; + } + } + + // Work around bold text changing implicit size + Layout.preferredWidth: boldMetrics.implicitWidth + Layout.preferredHeight: boldMetrics.implicitHeight * label.lineCount + Layout.fillWidth: true + + QQC2.Label { + id: boldMetrics + visible: false + text: parent.text + font.bold: true + font.family: Kirigami.Theme.smallFont.family + font.pointSize: Kirigami.Theme.smallFont.pointSize + horizontalAlignment: Text.AlignHCenter + wrapMode: QQC2.Label.Wrap + elide: Text.ElideMiddle + } + } + } +} \ No newline at end of file diff --git a/src/qml/Page/RoomList/Page.qml b/src/qml/Page/RoomList/Page.qml index 92f73385f..7c33a350c 100644 --- a/src/qml/Page/RoomList/Page.qml +++ b/src/qml/Page/RoomList/Page.qml @@ -13,8 +13,9 @@ import org.kde.kitemmodels 1.0 import org.kde.neochat 1.0 import './' as RoomList +import '../' as NeoChat -Kirigami.ScrollablePage { +Kirigami.Page { id: root /** @@ -23,7 +24,8 @@ Kirigami.ScrollablePage { * @note Other objects can access the value but the private function makes sure * that only the internal members can modify it. */ - readonly property int currentWidth: _private.currentWidth + readonly property int currentWidth: _private.currentWidth + spaceListWidth + readonly property alias spaceListWidth: spaceDrawer.width readonly property RoomListModel roomListModel: RoomListModel { connection: Controller.activeConnection @@ -37,24 +39,6 @@ Kirigami.ScrollablePage { sortFilterRoomListModel.filterText = ""; } - header: ColumnLayout { - visible: !root.collapsed - spacing: 0 - - RoomList.SpaceListView { - roomListModel: root.roomListModel - } - - Kirigami.Separator { - Layout.fillWidth: true - } - - Component { - id: spaceListContextMenu - SpaceListContextMenu {} - } - } - Connections { target: RoomManager function onCurrentRoomChanged() { @@ -106,135 +90,158 @@ Kirigami.ScrollablePage { collapsed: root.collapsed } - ListView { - id: listView + padding: 0 - activeFocusOnTab: true - clip: AccountRegistry.count > 1 + RowLayout { + anchors.fill: parent + spacing: 1 - header: QQC2.ItemDelegate { - width: visible ? ListView.view.width : 0 - height: visible ? Kirigami.Units.gridUnit * 2 : 0 - - visible: root.collapsed - - topPadding: Kirigami.Units.largeSpacing - leftPadding: Kirigami.Units.largeSpacing - rightPadding: Kirigami.Units.largeSpacing - bottomPadding: Kirigami.Units.largeSpacing - - onClicked: quickView.item.open(); - - Kirigami.Icon { - anchors.centerIn: parent - width: Kirigami.Units.iconSizes.smallMedium - height: Kirigami.Units.iconSizes.smallMedium - source: "search" - } - - Kirigami.Separator { - width: parent.width - anchors.bottom: parent.bottom - } + NeoChat.SpaceDrawer { + id: spaceDrawer + Layout.preferredWidth: spaceDrawer.enabled ? Kirigami.Units.gridUnit * 3 : 0 + Layout.fillHeight: true } - Layout.fillWidth: true + QQC2.ScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true - Kirigami.PlaceholderMessage { - anchors.centerIn: parent - width: parent.width - (Kirigami.Units.largeSpacing * 4) - visible: listView.count == 0 - text: sortFilterRoomListModel.filterText.length > 0 ? i18n("No rooms found") : i18n("Join some rooms to get started") - helpfulAction: Kirigami.Action { - icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add" - text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms") - onTriggered: pageStack.layers.push("qrc:/JoinRoomPage.qml", { - connection: Controller.activeConnection, - keyword: sortFilterRoomListModel.filterText - }) + background: Rectangle { + color: Kirigami.Theme.backgroundColor + Kirigami.Theme.colorSet: Kirigami.Theme.View } - } - ItemSelectionModel { - id: itemSelection - model: root.roomListModel - onCurrentChanged: listView.currentIndex = sortFilterRoomListModel.mapFromSource(current).row - } + ListView { + id: listView - model: SortFilterRoomListModel { - id: sortFilterRoomListModel + activeFocusOnTab: true + clip: AccountRegistry.count > 1 - sourceModel: root.roomListModel - roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories - onLayoutChanged: { - listView.currentIndex = sortFilterRoomListModel.mapFromSource(itemSelection.currentIndex).row - } - } + header: QQC2.ItemDelegate { + width: visible ? ListView.view.width : 0 + height: visible ? Kirigami.Units.gridUnit * 2 : 0 - section.property: sortFilterRoomListModel.filterText.length === 0 && !Config.mergeRoomList ? "category" : null - section.delegate: root.collapsed ? foldButton : sectionHeader + visible: root.collapsed - Component { - id: sectionHeader - Kirigami.ListSectionHeader { - height: implicitHeight - label: roomListModel.categoryName(section) - action: Kirigami.Action { - onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section)) - } - contentItem.children: QQC2.ToolButton { - icon { - name: roomListModel.categoryVisible(section) ? "go-up" : "go-down" - width: Kirigami.Units.iconSizes.small - height: Kirigami.Units.iconSizes.small - } + topPadding: Kirigami.Units.largeSpacing + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing - onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section)) - } - } - } - Component { - id: foldButton - Item { - width: ListView.view.width - height: visible ? width : 0 - QQC2.ToolButton { - id: button - anchors.centerIn: parent + onClicked: quickView.item.open(); - icon { - name: hovered ? (roomListModel.categoryVisible(section) ? "go-up" : "go-down") : roomListModel.categoryIconName(section) + Kirigami.Icon { + anchors.centerIn: parent width: Kirigami.Units.iconSizes.smallMedium height: Kirigami.Units.iconSizes.smallMedium + source: "search" } - onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section)) - - QQC2.ToolTip.text: roomListModel.categoryName(section) - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + Kirigami.Separator { + width: parent.width + anchors.bottom: parent.bottom + } } - } - } - reuseItems: true - currentIndex: -1 // we don't want any room highlighted by default + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + visible: listView.count == 0 + text: sortFilterRoomListModel.filterText.length > 0 ? i18n("No rooms found") : i18n("Join some rooms to get started") + helpfulAction: Kirigami.Action { + icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add" + text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms") + onTriggered: pageStack.layers.push("qrc:/JoinRoomPage.qml", { + connection: Controller.activeConnection, + keyword: sortFilterRoomListModel.filterText + }) + } + } - delegate: root.collapsed ? collapsedModeListComponent : normalModeListComponent + ItemSelectionModel { + id: itemSelection + model: root.roomListModel + onCurrentChanged: listView.currentIndex = sortFilterRoomListModel.mapFromSource(current).row + } - Component { - id: collapsedModeListComponent + model: SortFilterRoomListModel { + id: sortFilterRoomListModel - RoomList.CollapsedRoomDelegate { - filterText: sortFilterRoomListModel.filterText - } - } + sourceModel: root.roomListModel + roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories + onLayoutChanged: { + listView.currentIndex = sortFilterRoomListModel.mapFromSource(itemSelection.currentIndex).row + } + activeSpaceId: spaceDrawer.selectedSpaceId + } - Component { - id: normalModeListComponent + section.property: sortFilterRoomListModel.filterText.length === 0 && !Config.mergeRoomList ? "category" : null + section.delegate: root.collapsed ? foldButton : sectionHeader - RoomList.RoomDelegate { - filterText: sortFilterRoomListModel.filterText + Component { + id: sectionHeader + Kirigami.ListSectionHeader { + height: implicitHeight + label: roomListModel.categoryName(section) + action: Kirigami.Action { + onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section)) + } + contentItem.children: QQC2.ToolButton { + icon { + name: roomListModel.categoryVisible(section) ? "go-up" : "go-down" + width: Kirigami.Units.iconSizes.small + height: Kirigami.Units.iconSizes.small + } + + onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section)) + } + } + } + Component { + id: foldButton + Item { + width: ListView.view.width + height: visible ? width : 0 + QQC2.ToolButton { + id: button + anchors.centerIn: parent + + icon { + name: hovered ? (roomListModel.categoryVisible(section) ? "go-up" : "go-down") : roomListModel.categoryIconName(section) + width: Kirigami.Units.iconSizes.smallMedium + height: Kirigami.Units.iconSizes.smallMedium + } + + onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section)) + + QQC2.ToolTip.text: roomListModel.categoryName(section) + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } + } + } + + reuseItems: true + currentIndex: -1 // we don't want any room highlighted by default + + delegate: root.collapsed ? collapsedModeListComponent : normalModeListComponent + + Component { + id: collapsedModeListComponent + + RoomList.CollapsedRoomDelegate { + filterText: sortFilterRoomListModel.filterText + } + } + + Component { + id: normalModeListComponent + + RoomList.RoomDelegate { + filterText: sortFilterRoomListModel.filterText + } + } } } } @@ -269,7 +276,7 @@ Kirigami.ScrollablePage { // we moved to the right if (_private.currentWidth < _private.collapseWidth && _private.currentWidth + (mouse.x - _lastX) >= _private.collapseWidth) { // Here we get back directly to a more wide mode. - _private.currentWidth = _private.collapseWidth; + _private.currentWidth = _private.defaultWidth; Config.collapsed = false; } else if (_private.currentWidth >= _private.collapseWidth) { // Increase page width @@ -278,7 +285,7 @@ Kirigami.ScrollablePage { } else if (mouse.x < _lastX) { const tmpWidth = _private.currentWidth - (_lastX - mouse.x); if (tmpWidth < _private.collapseWidth) { - _private.currentWidth = Qt.binding(() => _private.collapsedSize + (root.contentItem.QQC2.ScrollBar.vertical.visible ? root.contentItem.QQC2.ScrollBar.vertical.width : 0)); + _private.currentWidth = Qt.binding(() => _private.collapsedSize); Config.collapsed = true; } else { _private.currentWidth = tmpWidth; @@ -296,6 +303,6 @@ Kirigami.ScrollablePage { property int currentWidth: Config.collapsed ? collapsedSize : defaultWidth readonly property int defaultWidth: Kirigami.Units.gridUnit * 17 readonly property int collapseWidth: Kirigami.Units.gridUnit * 10 - readonly property int collapsedSize: Kirigami.Units.gridUnit * 3 - Kirigami.Units.smallSpacing * 3 + readonly property int collapsedSize: Kirigami.Units.gridUnit * 3 - Kirigami.Units.smallSpacing * 3 + (scrollView.QQC2.ScrollBar.vertical.visible ? scrollView.QQC2.ScrollBar.vertical.width : 0) } } diff --git a/src/qml/Page/RoomList/SpaceDrawer.qml b/src/qml/Page/RoomList/SpaceDrawer.qml new file mode 100644 index 000000000..788bfd886 --- /dev/null +++ b/src/qml/Page/RoomList/SpaceDrawer.qml @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2020-2023 Tobias Fella +// SPDX-FileCopyrightText: 2021-2022 Bart De Vries +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.20 as Kirigami + +import '.' +import org.kde.neochat 1.0 + +QQC2.Control { + id: root + + readonly property real pinnedWidth: Kirigami.Units.gridUnit * 6 + readonly property int buttonDisplayMode: Kirigami.NavigationTabButton.IconOnly + property bool drawerEnabled: true + + Kirigami.Theme.colorSet: Kirigami.Theme.Window + Kirigami.Theme.inherit: false + + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + property string selectedSpaceId + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + } + + contentItem: Loader { + id: sidebarColumn + active: root.drawerEnabled + z: 0 + + sourceComponent: ColumnLayout { + spacing: 0 + + QQC2.ScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true + + QQC2.ScrollBar.vertical.policy: QQC2.ScrollBar.AlwaysOff + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + contentWidth: -1 // disable horizontal scroll + + ColumnLayout { + id: column + width: scrollView.width + spacing: 0 + + Kirigami.NavigationTabButton { + Layout.fillWidth: true + Layout.preferredHeight: width + display: Kirigami.NavigationTabButton.IconOnly + text: i18n("All Rooms") + icon.name: "globe" + checked: true + onClicked: root.selectedSpaceId = "" + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: text + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } + + Repeater { + model: SortFilterSpaceListModel { + sourceModel: RoomListModel { + connection: Controller.activeConnection + } + } + onCountChanged: root.enabled = count > 0 + + delegate: AvatarTabButton { + Layout.fillWidth: true + Layout.preferredHeight: width + display: Kirigami.NavigationTabButton.IconOnly + text: model.name + source: "image://mxc/" + model.avatar + name: model.name + onClicked: root.selectedSpaceId = model.id + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: text + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + onPressAndHold: root.createContextMenu(model.currentRoom) + + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse + onTapped: root.createContextMenu(model.currentRoom) + } + } + } + } + } + } + } + + function createContextMenu(room) { + let context = spaceListContextMenu.createObject(root, { + room: room + }); + context.open() + } + Component { + id: spaceListContextMenu + SpaceListContextMenu {} + } +} diff --git a/src/res.qrc b/src/res.qrc index e95793009..7df446579 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -11,7 +11,7 @@ qml/Page/RoomList/CollapsedRoomDelegate.qml qml/Page/RoomList/RoomDelegate.qml qml/Page/RoomList/Page.qml - qml/Page/RoomList/SpaceListContextMenu.qml + qml/Page/RoomList/SpaceListContextMenu.qml qml/Page/RoomList/SpaceDelegate.qml qml/Page/RoomList/SpaceListView.qml qml/Page/RoomList/UserInfo.qml @@ -122,5 +122,7 @@ qml/Component/ChatBox/LocationChooser.qml qml/Component/TimelineView.qml qml/Component/InvitationView.qml + qml/Component/AvatarTabButton.qml + qml/Page/RoomList/SpaceDrawer.qml diff --git a/src/spacehierarchycache.cpp b/src/spacehierarchycache.cpp index 91b182901..37b342bd7 100644 --- a/src/spacehierarchycache.cpp +++ b/src/spacehierarchycache.cpp @@ -32,13 +32,13 @@ void SpaceHierarchyCache::cacheSpaceHierarchy() return; } - const auto roomList = connection->allRooms(); + const auto &roomList = connection->allRooms(); for (const auto &room : roomList) { const auto neoChatRoom = static_cast(room); if (neoChatRoom->isSpace()) { populateSpaceHierarchy(neoChatRoom->id()); } else { - connect(neoChatRoom, &Room::baseStateLoaded, neoChatRoom, [this, neoChatRoom]() { + connectSingleShot(neoChatRoom, &Room::baseStateLoaded, neoChatRoom, [this, neoChatRoom]() { if (neoChatRoom->isSpace()) { populateSpaceHierarchy(neoChatRoom->id()); } @@ -55,7 +55,7 @@ void SpaceHierarchyCache::populateSpaceHierarchy(const QString &spaceId) return; } #ifdef QUOTIENT_07 - GetSpaceHierarchyJob *job = connection->callApi(spaceId); + auto job = connection->callApi(spaceId); connect(job, &BaseJob::success, this, [this, job, spaceId]() { const auto rooms = job->rooms(); @@ -64,7 +64,6 @@ void SpaceHierarchyCache::populateSpaceHierarchy(const QString &spaceId) for (const auto &state : rooms[i].childrenState) { roomList.push_back(state->stateKey()); } - roomList.push_back(rooms.at(i).roomId); } m_spaceHierarchy.insert(spaceId, roomList); Q_EMIT spaceHierarchyChanged(); @@ -97,3 +96,14 @@ QVector &SpaceHierarchyCache::getRoomListForSpace(const QString &spaceI } return m_spaceHierarchy[spaceId]; } + +bool SpaceHierarchyCache::isChildSpace(const QString &spaceId) const +{ + const auto childrens = m_spaceHierarchy.values(); + for (const auto &children : childrens) { + if (children.contains(spaceId)) { + return true; + } + } + return false; +} diff --git a/src/spacehierarchycache.h b/src/spacehierarchycache.h index 5f45bc0f9..926719302 100644 --- a/src/spacehierarchycache.h +++ b/src/spacehierarchycache.h @@ -36,6 +36,11 @@ public: */ [[nodiscard]] QVector &getRoomListForSpace(const QString &spaceId, bool updateCache); + /** + * @brief Returns whether the space is a child space of any other space. + */ + [[nodiscard]] bool isChildSpace(const QString &spaceId) const; + Q_SIGNALS: void spaceHierarchyChanged();