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:
parent
0b47e6ff8a
commit
a83f1114e2
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue