chore(Communities/InDropdown): performance improved

- removed multiple filtering separately for each category
- no instantiation of all delegates up front (regular ListView approach
  used)
- fixed category selection when filtering - selecting/deselecting only
  items fulfilling search criteria
- significant simplification of the code

Closes: #14275
This commit is contained in:
Michał Cieślak 2024-04-05 12:45:50 +02:00 committed by Michał
parent 0b47e6ff8a
commit a83f1114e2
3 changed files with 238 additions and 270 deletions

View File

@ -1,6 +1,6 @@
import QtQuick 2.14 import QtQuick 2.15
import QtQuick.Controls 2.14 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.15
import AppLayouts.Communities.popups 1.0 import AppLayouts.Communities.popups 1.0

View File

@ -81,6 +81,86 @@ ListModel {
icon: "" icon: ""
colorId: 4 colorId: 4
} }
ListElement {
itemId: "_report-scam2"
isCategory: false
categoryId: "_support"
name: "report-scam2"
emoji: ""
color: ""
icon: ""
colorId: 4
}
ListElement {
itemId: "_report-scam3"
isCategory: false
categoryId: "_support"
name: "report-scam3"
emoji: ""
color: ""
icon: ""
colorId: 4
}
ListElement {
itemId: "_report-scam4"
isCategory: false
categoryId: "_support"
name: "report-scam4"
emoji: ""
color: ""
icon: ""
colorId: 4
}
ListElement {
itemId: "_report-scam5"
isCategory: false
categoryId: "_support"
name: "report-scam5"
emoji: ""
color: ""
icon: ""
colorId: 4
}
ListElement {
itemId: "_report-scam6"
isCategory: false
categoryId: "_support"
name: "report-scam6"
emoji: ""
color: ""
icon: ""
colorId: 4
}
ListElement {
itemId: "_report-scam7"
isCategory: false
categoryId: "_support"
name: "report-scam7"
emoji: ""
color: ""
icon: ""
colorId: 4
}
ListElement {
itemId: "_report-scam8"
isCategory: false
categoryId: "_support"
name: "report-scam8"
emoji: ""
color: ""
icon: ""
colorId: 4
}
ListElement {
itemId: "_report-scam9"
isCategory: false
categoryId: "_support"
name: "report-scam9"
emoji: ""
color: ""
icon: ""
colorId: 4
}
ListElement { ListElement {
itemId: "" itemId: ""
isCategory: true isCategory: true

View File

@ -1,20 +1,28 @@
import QtQuick 2.14 import QtQuick 2.15
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.15
import StatusQ.Core 0.1 import StatusQ 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Popups 0.1 import StatusQ.Popups 0.1
import shared.controls 1.0
import SortFilterProxyModel 0.2
import AppLayouts.Communities.controls 1.0 import AppLayouts.Communities.controls 1.0
import shared.controls 1.0
import SortFilterProxyModel 0.2
StatusDropdown { StatusDropdown {
id: root id: root
width: 289
padding: 8
// force keeping within the bounds of the enclosing window
margins: 0
property bool allowChoosingEntireCommunity: false property bool allowChoosingEntireCommunity: false
property bool showAddChannelButton: false property bool showAddChannelButton: false
@ -30,23 +38,97 @@ StatusDropdown {
Add, Update Add, Update
} }
width: 289
padding: 8
// force keeping within the bounds of the enclosing window
margins: 0
signal addChannelClicked signal addChannelClicked
signal communitySelected signal communitySelected
signal channelsSelected(var channels) signal channelsSelected(var channels)
function setSelectedChannels(channels) { function setSelectedChannels(channels) {
d.setSelectedChannels(channels) d.selectedChannels.clear()
channels.forEach(c => d.selectedChannels.add(c))
d.selectedChannelsChanged()
} }
onAboutToHide: searcher.text = "" onAboutToHide: {
onAboutToShow: scrollView.Layout.preferredHeight = Math.min( searcher.text = ""
scrollView.implicitHeight, 420) listView.positionViewAtBeginning()
}
onAboutToShow: listView.Layout.preferredHeight = Math.min(
listView.implicitHeight, 420)
// only channels (no entries representing categories), sorted according to
// category position and position
SortFilterProxyModel {
id: onlyChannelsModel
sourceModel: root.model
filters: ValueFilter {
roleName: "isCategory"
value: false
}
sorters: [
RoleSorter {
roleName: "categoryPosition"
priority: 2 // Higher number -> higher priority
},
RoleSorter {
roleName: "position"
priority: 1
}
]
}
// only items representing categories
SortFilterProxyModel {
id: categoriesModel
sourceModel: root.model
filters: ValueFilter {
roleName: "isCategory"
value: true
}
}
// categories, name role renamed to categoryName
RolesRenamingModel {
id: categoriesModelRenamed
sourceModel: categoriesModel
mapping: RoleRename {
from: "name"
to: "categoryName"
}
}
// categories joined to channels model in order to provide channelName,
// in order to be used in section.property.
LeftJoinModel {
id: joined
leftModel: onlyChannelsModel
rightModel: categoriesModelRenamed
joinRole: "categoryId"
rolesToJoin: "categoryName"
}
// final filtering based on user's input in search bar
SortFilterProxyModel {
id: filtered
sourceModel: joined
filters: RegExpFilter {
roleName: "name"
pattern: `*${searcher.text}*`
caseSensitivity : Qt.CaseInsensitive
syntax: RegExpFilter.Wildcard
}
}
QtObject { QtObject {
id: d id: d
@ -56,12 +138,6 @@ StatusDropdown {
readonly property int itemStandardHeight: 44 readonly property int itemStandardHeight: 44
readonly property var selectedChannels: new Set() readonly property var selectedChannels: new Set()
signal setSelectedChannels(var channels)
function search(text, searcherText) {
return text.toLowerCase().includes(searcherText.toLowerCase())
}
function resolveEmoji(emoji) { function resolveEmoji(emoji) {
return !!emoji ? emoji : "" return !!emoji ? emoji : ""
} }
@ -69,16 +145,6 @@ StatusDropdown {
function resolveColor(color, colorId) { function resolveColor(color, colorId) {
return !!color ? color : Theme.palette.userCustomizationColors[colorId] return !!color ? color : Theme.palette.userCustomizationColors[colorId]
} }
function addToSelectedChannels(model) {
selectedChannels.add(model.itemId)
selectedChannelsChanged()
}
function removeFromSelectedChannels(model) {
selectedChannels.delete(model.itemId)
selectedChannelsChanged()
}
} }
contentItem: ColumnLayout { contentItem: ColumnLayout {
@ -150,26 +216,25 @@ StatusDropdown {
visible: root.allowChoosingEntireCommunity visible: root.allowChoosingEntireCommunity
} }
StatusScrollView {
id: scrollView StatusListView {
id: listView
model: filtered
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: Math.min(d.maxHeightCountNo * d.itemStandardHeight, contentHeight) Layout.minimumHeight: Math.min(d.maxHeightCountNo * d.itemStandardHeight,
contentHeight)
Layout.maximumHeight: Layout.minimumHeight Layout.maximumHeight: Layout.minimumHeight
contentWidth: availableWidth
Layout.bottomMargin: d.defaultVMargin Layout.bottomMargin: d.defaultVMargin
Layout.topMargin: Layout.topMargin: !root.allowChoosingEntireCommunity
!root.allowChoosingEntireCommunity && !root.allowChoosingEntireCommunity ? d.defaultVMargin : 0 && !root.allowChoosingEntireCommunity ? d.defaultVMargin : 0
padding: 0 Component {
id: addChannelButtonComponent
ColumnLayout {
id: scrollableColumn
width: scrollView.availableWidth
spacing: 0
StatusIconTextButton { StatusIconTextButton {
Layout.preferredHeight: 36 height: 36
visible: root.showAddChannelButton
leftPadding: 8 leftPadding: 8
spacing: 8 spacing: 8
statusIcon: "add" statusIcon: "add"
@ -179,240 +244,63 @@ StatusDropdown {
text: qsTr("Add channel") text: qsTr("Add channel")
onClicked: root.addChannelClicked() onClicked: root.addChannelClicked()
} }
}
Repeater { header: root.showAddChannelButton ? addChannelButtonComponent : null
id: topRepeater
model: SortFilterProxyModel {
id: topLevelModel
sourceModel: root.model section.delegate: CategoryListItem {
title: section
filters: AnyOf { width: ListView.view.width
ValueFilter {
roleName: "categoryId"
value: ""
}
ValueFilter {
roleName: "isCategory"
value: true
}
}
sorters: [ MouseArea {
RoleSorter { anchors.fill: parent
roleName: "categoryPosition"
priority: 2 // Higher number === higher priority
},
RoleSorter {
roleName: "position"
priority: 1
}
]
}
ColumnLayout { onClicked: {
id: column const categoryId = ModelUtils.getByKey(
categoriesModel, "name", section, "categoryId")
const allKeys = ModelUtils.modelToArray(
filtered, ["itemId", "categoryId"])
const inCategoryKeys = allKeys.filter(
e => e.categoryId === categoryId)
const allSelected = inCategoryKeys.every(
e => d.selectedChannels.has(e.itemId))
readonly property var topModel: model if (allSelected)
readonly property alias checkBox: loader.item inCategoryKeys.forEach(
property int checkedCount: 0 e => d.selectedChannels.delete(e.itemId))
else
inCategoryKeys.forEach(
e => d.selectedChannels.add(e.itemId))
readonly property bool isCategory: model.isCategory d.selectedChannelsChanged()
readonly property string categoryId: model.categoryId
Layout.fillWidth: true
spacing: 0
visible: {
if (!isCategory)
return d.search(model.name, searcher.text)
const subItemsCount = subItemsRepeater.count
for (let i = 0; i < subItemsCount; i++)
if (subItemsRepeater.itemAt(i).show)
return true
return false
}
Loader {
id: loader
Layout.fillWidth: true
Layout.preferredHeight: d.itemStandardHeight
Layout.topMargin: isCategory ? d.defaultVMargin : 0
sourceComponent: isCategory
? communityCategoryDelegate
: communityDelegate
Connections {
target: radioButton
function onToggled() {
const checkBox = loader.item.checkBox
checkBox.checked = false
checkBox.onToggled()
}
}
Component {
id: communityDelegate
CommunityListItem {
id: communityItem
title: "#" + model.name
asset.name: model.icon ?? ""
asset.emoji: d.resolveEmoji(model.emoji)
asset.color: d.resolveColor(model.color,
model.colorId)
checkBox.onToggled: {
if (checked)
radioButton.checked = false
}
checkBox.onCheckedChanged: {
if (checkBox.checked)
d.addToSelectedChannels(model)
else
d.removeFromSelectedChannels(model)
}
Connections {
target: d
function onSetSelectedChannels(channels) {
communityItem.checked = channels.includes(
model.itemId)
}
}
}
}
Component {
id: communityCategoryDelegate
CategoryListItem {
title: model.name
checkState: {
if (checkedCount === subItems.count)
return Qt.Checked
else if (checkedCount === 0)
return Qt.Unchecked
return Qt.PartiallyChecked
}
checkBox.onToggled: {
if (checked)
radioButton.checked = false
subItemsRepeater.setAll(checkState)
}
}
}
}
SortFilterProxyModel {
id: subItems
sourceModel: isCategory ? root.model : null
filters: AllOf {
ValueFilter {
roleName: "categoryId"
value: column.categoryId
}
ValueFilter {
roleName: "isCategory"
value: false
}
}
sorters: RoleSorter {
roleName: "position"
}
}
Repeater {
id: subItemsRepeater
model: subItems
function setAll(checkState) {
const subItemsCount = count
for (let i = 0; i < subItemsCount; i++) {
itemAt(i).checkState = checkState
}
}
CommunityListItem {
id: communitySubItem
readonly property bool show:
d.search(model.name, searcher.text)
Layout.fillWidth: true
visible: show
title: "#" + model.name
asset.name: model.icon ?? ""
asset.emoji: d.resolveEmoji(model.emoji)
asset.color: d.resolveColor(model.color,
model.colorId)
onCheckedChanged: {
if (checked) {
radioButton.checked = false
d.addToSelectedChannels(model)
} else {
d.removeFromSelectedChannels(model)
}
Qt.callLater(() => checkedCount += checked ? 1 : -1)
}
Connections {
target: d
function onSetSelectedChannels(channels) {
communitySubItem.checked = channels.includes(
model.itemId)
}
}
}
}
} }
} }
}
StatusBaseText { section.property: "categoryName"
id: noContactsText
horizontalAlignment: Text.AlignHCenter delegate: CommunityListItem {
verticalAlignment: Text.AlignVCenter id: communitySubItem
visible: { width: ListView.view.width
for (let i = 0; i < topRepeater.count; i++) { visible: show
const item = topRepeater.itemAt(i)
if (item && item.visible)
return false
}
return true title: "#" + model.name
}
text: qsTr("No channels found") asset.name: model.icon ?? ""
color: Theme.palette.baseColor1 asset.emoji: d.resolveEmoji(model.emoji)
font.pixelSize: Theme.tertiaryTextFontSize asset.color: d.resolveColor(model.color, model.colorId)
elide: Text.ElideRight
lineHeight: 1.2 checked: d.selectedChannels.has(model.itemId)
checkBox.onToggled: {
if (checked)
d.selectedChannels.add(model.itemId)
else
d.selectedChannels.delete(model.itemId)
d.selectedChannelsChanged()
} }
} }
} }