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 Jonathan Rainville
parent bc878be2a7
commit 2ee103a17c
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 {
id: topRepeater
model: SortFilterProxyModel {
id: topLevelModel
sourceModel: root.model
filters: AnyOf {
ValueFilter {
roleName: "categoryId"
value: ""
}
ValueFilter {
roleName: "isCategory"
value: true
}
} }
sorters: [ header: root.showAddChannelButton ? addChannelButtonComponent : null
RoleSorter {
roleName: "categoryPosition"
priority: 2 // Higher number === higher priority
},
RoleSorter {
roleName: "position"
priority: 1
}
]
}
ColumnLayout { section.delegate: CategoryListItem {
id: column title: section
readonly property var topModel: model width: ListView.view.width
readonly property alias checkBox: loader.item
property int checkedCount: 0
readonly property bool isCategory: model.isCategory MouseArea {
readonly property string categoryId: model.categoryId anchors.fill: parent
Layout.fillWidth: true onClicked: {
spacing: 0 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))
visible: { if (allSelected)
if (!isCategory) inCategoryKeys.forEach(
return d.search(model.name, searcher.text) e => d.selectedChannels.delete(e.itemId))
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 else
d.removeFromSelectedChannels(model) inCategoryKeys.forEach(
} e => d.selectedChannels.add(e.itemId))
Connections { d.selectedChannelsChanged()
target: d
function onSetSelectedChannels(channels) {
communityItem.checked = channels.includes(
model.itemId)
}
} }
} }
} }
Component { section.property: "categoryName"
id: communityCategoryDelegate
CategoryListItem { delegate: CommunityListItem {
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 id: communitySubItem
readonly property bool show: width: ListView.view.width
d.search(model.name, searcher.text)
Layout.fillWidth: true
visible: show visible: show
title: "#" + model.name title: "#" + model.name
asset.name: model.icon ?? "" asset.name: model.icon ?? ""
asset.emoji: d.resolveEmoji(model.emoji) asset.emoji: d.resolveEmoji(model.emoji)
asset.color: d.resolveColor(model.color, asset.color: d.resolveColor(model.color, model.colorId)
model.colorId)
onCheckedChanged: { checked: d.selectedChannels.has(model.itemId)
if (checked) {
radioButton.checked = false
d.addToSelectedChannels(model)
} else {
d.removeFromSelectedChannels(model)
}
Qt.callLater(() => checkedCount += checked ? 1 : -1) checkBox.onToggled: {
} if (checked)
d.selectedChannels.add(model.itemId)
else
d.selectedChannels.delete(model.itemId)
Connections { d.selectedChannelsChanged()
target: d
function onSetSelectedChannels(channels) {
communitySubItem.checked = channels.includes(
model.itemId)
}
}
}
}
}
}
StatusBaseText {
id: noContactsText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
visible: {
for (let i = 0; i < topRepeater.count; i++) {
const item = topRepeater.itemAt(i)
if (item && item.visible)
return false
}
return true
}
text: qsTr("No channels found")
color: Theme.palette.baseColor1
font.pixelSize: Theme.tertiaryTextFontSize
elide: Text.ElideRight
lineHeight: 1.2
} }
} }
} }