Dynamicaly manage actions in a Cascades multi-selection handler - qt

I try to handle a set of actions for multiple items selection on a ListView. The context: a RSS reader. An article can be read/unread, marked/unmarked, published/unpublished. Currently, my code adds every possible action when the multi-selection is started by the user: mark as read, keep unread, … So mark as read is available even if every selected article is already marked as read.
I'm trying to hide irrelevant actions. There's an example in the documentation to switch between multiple selection handlers, with predefined action lists. I would need to create an handler for every possibility, so 8 handlers. That's clearly not the solution.
I tried to call MultiSelectionHandler::removeAllActions() every time my selection changes and MultiSelectionHandler::addAction() to add back needed actions. This is the relevant part of my current code:
ListView {
dataModel: _articleModel
id: listView
multiSelectAction: MultiSelectActionItem {
}
multiSelectHandler {
status: qsTr("None selected")
actions: []
attachedObjects: [
ActionItem {
id: actionMultiRead
title: qsTr("Mark as read")
imageSource: "asset:///images/mark_as_read.png"
onTriggered: {
var selectionList = listView.selectionList();
listView.clearSelection();
for (var i = 0; i < selectionList.length; ++i)
_articleModel.data(selectionList[i]).unread = false;
}
},
ActionItem {
id: actionMultiUnread
title: qsTr("Keep unread")
imageSource: "asset:///images/keep_unread.png"
onTriggered: {
var selectionList = listView.selectionList();
listView.clearSelection();
for (var i = 0; i < selectionList.length; ++ i)
_articleModel.data(selectionList[i]).unread = true;
}
},
ActionItem {
id: actionMultiPublish
title: qsTr("Publish")
imageSource: "asset:///images/publish.png"
onTriggered: {
var selectionList = listView.selectionList();
listView.clearSelection();
for (var i = 0; i < selectionList.length; ++ i)
_articleModel.data(selectionList[i]).published = true;
}
},
ActionItem {
id: actionMultiUnpublish
title: qsTr("Unpublish")
imageSource: "asset:///images/unpublish.png"
onTriggered: {
var selectionList = listView.selectionList();
listView.clearSelection();
for (var i = 0; i < selectionList.length; ++ i)
_articleModel.data(selectionList[i]).published = false;
}
},
ActionItem {
id: actionMultiStar
title: qsTr("Star")
imageSource: "asset:///images/star.png"
onTriggered: {
var selectionList = listView.selectionList();
listView.clearSelection();
for (var i = 0; i < selectionList.length; ++ i)
_articleModel.data(selectionList[i]).marked = true;
}
},
ActionItem {
id: actionMultiUnstar
title: qsTr("Unstar")
imageSource: "asset:///images/unstar.png"
onTriggered: {
var selectionList = listView.selectionList();
listView.clearSelection();
for (var i = 0; i < selectionList.length; ++ i)
_articleModel.data(selectionList[i]).marked = false;
}
}
]
}
onSelectionChanged: {
if (selectionList().length > 1) {
multiSelectHandler.status = qsTr("%1 items selected").arg(selectionList().length);
} else if (selectionList().length == 1) {
multiSelectHandler.status = qsTr("1 item selected");
} else {
multiSelectHandler.status = qsTr("None selected");
}
// Update available actions
multiSelectHandler.removeAllActions();
for (var i = 0; i < selectionList().length; ++ i) {
var elt = _articleModel.data(selectionList()[i]);
if (elt.marked) {
multiSelectHandler.addAction(actionMultiUnstar);
console.log("Adding unstar");
} else {
multiSelectHandler.addAction(actionMultiStar);
console.log("Adding star");
}
if (elt.published) {
multiSelectHandler.addAction(actionMultiUnpublish);
console.log("Adding unpublish");
} else {
multiSelectHandler.addAction(actionMultiPublish);
console.log("Adding publish");
}
if (elt.unread) {
multiSelectHandler.addAction(actionMultiRead);
console.log("Adding read");
} else {
multiSelectHandler.addAction(actionMultiUnread);
console.log("Adding unread");
}
}
}
}
While it could greatly be optimized and works fine until the selection changes, it doesn't work anymore after a change: MultiSelectionHandler::removeAllActions() delete the actions, they can't be added back.
Is there any way to achieve this without declaring 8 multi selection handlers?

the question is quite old, and probably you have already solved, but for any other one who stumbled up on this question my solution is to use MultiSelectionHandler::removeAction() instead of removeAllActions().
removeAllActions automatically delete the object, thus one should recreated the actions every time.
for (var i = 0; i < multiSelectAction.actionCount(); ++ i)
multiSelectAction.removeAction(multiSelectAction.actionAt(i));
Cheers.

Related

Is it possible to create a LazyVGrid without the Lazy modifier?

I'm loading data in from my Firebase backend, the "lazy" part makes my app look glitchy/frozen-like when scrolling down, it lags heavily...
Is it possible to create a VGrid "without the lazy functionality"??
(iOS 14)
If not, any suggestions other than ditching the Grid look altogether?
let layout = [
GridItem(.flexible()),
GridItem(.flexible()),
]
#ObservedObject var homeModel = Home_ViewModel()
NavigationView(content: {
ScrollView() {
LazyVGrid(columns: layout, spacing: 10) {
ForEach(homeModel.projectList) { item in
ProjectItemWidget(
projectID: item.id,
projectTitle: item.projectTitle,
projectAuthorProfileImage: item.authorProfileImageUrl,
projectAuthor: item.projectAuthor)
}
}
.padding(.trailing, 7.5)
}
}
I was struggling thinking in a solution where a could create a grid layout without using LazyVGrid and came up with the following:
extension Array {
func getElementAt(index: Int) -> Element? {
return (index < self.endIndex) ? self[index] : nil
}
}
struct CustomGridLayout<Element, GridCell>: View where GridCell: View {
private var array: [Element]
private var numberOfColumns: Int
private var gridCell: (_ element: Element) -> GridCell
init(_ array: [Element], numberOfColumns: Int, #ViewBuilder gridCell: #escaping (_ element: Element) -> GridCell) {
self.array = array
self.numberOfColumns = numberOfColumns
self.gridCell = gridCell
}
var body: some View {
Grid {
ForEach(Array(stride(from: 0, to: self.array.count, by: self.numberOfColumns)), id: \.self) { index in
GridRow {
ForEach(0..<self.numberOfColumns, id: \.self) { j in
if let element = self.array.getElementAt(index: index + j) {
self.gridCell(element)
}
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
An example using in a View:
struct ContentView: View {
private var array: [Int] = Array(1...7)
var body: some View {
CustomGridLayout(array, numberOfColumns: 3) { element in
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.orange)
.overlay(alignment: .center) {
Text("\(element)")
}
}
.padding(.horizontal)
}
}
You can see the result in the following link: https://i.stack.imgur.com/1o7ip.png

Can't create a simple selectable tag view in SwiftUI

I'm not so familiar with SwfitUI.
I found this helper to achieve this tag view:
But making it selectable is pain & for everything I did, I got many errors...
How can I make this thing work!?
Here is my complete class:
import SwiftUI
struct TagViewItem: Hashable {
var title: String
var isSelected: Bool
static func == (lhs: TagViewItem, rhs: TagViewItem) -> Bool {
return lhs.isSelected == rhs.isSelected
}
func hash(into hasher: inout Hasher) {
hasher.combine(title)
hasher.combine(isSelected)
}
}
struct TagView: View {
#State var tags: [TagViewItem]
#State private var totalHeight = CGFloat.zero // << variant for ScrollView/List // = CGFloat.infinity // << variant for VStack
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)// << variant for ScrollView/List
//.frame(maxHeight: totalHeight) // << variant for VStack
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(tags.indices) { index in
item(for: tags[index].title, isSelected: &tags[index].isSelected)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width) {
width = 0
height -= d.height
}
let result = width
if tag == self.tags.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tag == self.tags.last! {
height = 0 // last item
}
return result
})
}
}.background(viewHeightReader($totalHeight))
}
private func item(for text: String, isSelected: inout Bool) -> some View {
Text(text)
.foregroundColor(isSelected ? Colors.primaryBarBackground : Colors.textColor)
.padding()
.lineLimit(1)
.background(isSelected ? Colors.primaryBlue : Colors.primaryBarBackground)
.frame(height: 36)
.cornerRadius(18)
.overlay(Capsule().stroke(Colors.primaryBlue, lineWidth: 4))
.onTapGesture {
isSelected.toggle()
}
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}
Finally, after wrestling with it a lot, it is the working version:
struct TagView: View {
#State var tags: [TagViewItem]
#State private var totalHeight = CGFloat.zero // << variant for ScrollView/List // = CGFloat.infinity // << variant for VStack
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)// << variant for ScrollView/List
//.frame(maxHeight: totalHeight) // << variant for VStack
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(tags.indices) { index in
item(for: tags[index].title, isSelected: tags[index].isSelected)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width) {
width = 0
height -= d.height
}
let result = width
if tags[index].title == self.tags.last!.title {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tags[index].title == self.tags.last!.title {
height = 0 // last item
}
return result
}).onTapGesture {
tags[index].isSelected.toggle()
}
}
}.background(viewHeightReader($totalHeight))
}
private func item(for text: String, isSelected: Bool) -> some View {
Text(text)
.foregroundColor(isSelected ? Colors.primaryBarBackground : Colors.textColor)
.padding()
.lineLimit(1)
.background(isSelected ? Colors.primaryBlue : Colors.primaryBarBackground)
.frame(height: 36)
.cornerRadius(18)
.overlay(Capsule().stroke(Colors.primaryBlue, lineWidth: 1))
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}
Usage:
TagView(tags: [TagViewItem(title: "ff", isSelected: false), TagViewItem(title: "yyhuuuh", isSelected: false), TagViewItem(title: "kjhgdtfyughuihu", isSelected: true), TagViewItem(title: "nbyvyvuyv", isSelected: false)])

SWIFTUI Call Key Dictionary not work with the error: 'Subscript index of type '() -> Bool' in a key path must be Hashable'

I have this view:
import SwiftUI
struct SectionView1: View {
let dateStr:String
#Binding var isSectionView:Bool
var body: some View {
HStack {
Button(action: {
self.isSectionView.toggle()
}) {
Image(systemName: isSectionView ? "chevron.down.circle" : "chevron.right.circle")
}
Text("Media del \(dateStr)")
}
}
}
which will be called from view:
import SwiftUI
import Photos
struct MediaView: View {
let geoFolder:GeoFolderCD
#State private var assetsForDate = [String :[PHAsset]]()
#State private var isSectionViewArray:[String:Bool] = [:]
var body: some View {
List {
ForEach(assetsForDate.keys.sorted(by: > ), id: \.self) { dateStr in
Section {
SectionView1(dateStr: dateStr,
isSectionView: self.$isSectionViewArray[dateStr, default: true])
}
}
}
.onAppear {
self.assetsForDate = FetchMediaUtility().fetchGeoFolderAssetsForDate(geoFolder: geoFolderStruct, numAssets: numMediaToFetch)
for dateStr in self.assetsForDate.keys.sorted() {
self.isSectionViewArray[dateStr] = true
}
}
}
}
but I have the error: Subscript index of type '() -> Bool' in a key path must be Hashable in isSectionView: self.$isSectionViewArray[dateStr, default: true]
Why isSectionViewArray:[String:Bool] = [:] is not Hasbable?
How can modify the code for work?
If I remove, in SectionView, #Binding var isSectionView:Bool, the code work fine, or if I set, from SectionView, #Binding var isSectionViewArray:[String:Bool] = [:], the code work fine.
You can write your own binding with the below code and it should work
var body: some View {
List {
ForEach(assetsForDate.keys.sorted(by: > ), id: \.self) { dateStr in
let value = Binding<Bool>(get: { () -> Bool in
return self.isSectionViewArray[dateStr, default: true]
}) { (value) in
}
Section {
SectionView1(dateStr: dateStr,
isSectionView: value)
}
}
}
.onAppear {
self.assetsForDate = FetchMediaUtility().fetchGeoFolderAssetsForDate(geoFolder: geoFolderStruct, numAssets: numMediaToFetch)
for dateStr in self.assetsForDate.keys.sorted() {
self.isSectionViewArray[dateStr] = true
}
}
}

Hide key from Qt Virtual keyboard

Is there a way to hide the language selection key from the virtual keyboard without use a custom layout?
I was able to hide the language key with a workaround:
property var keyboardLayout: inputPanel.keyboard.layout
function findChildByProperty(parent, propertyName, propertyValue, compareCb) {
var obj = null
if (parent === null)
return null
var children = parent.children
for (var i = 0; i < children.length; i++) {
obj = children[i]
if (obj.hasOwnProperty(propertyName)) {
if (compareCb !== null) {
if (compareCb(obj[propertyName], propertyValue))
break
} else if (obj[propertyName] === propertyValue) {
break
}
}
obj = findChildByProperty(obj, propertyName, propertyValue, compareCb)
if (obj)
break
}
return obj
}
onKeyboardLayoutChanged: {
if(keyboardLayout!=""){
var ChangeLanguageKey= findChildByProperty(inputPanel.keyboard, "objectName", "changeLanguageKey", null)
if(ChangeLanguageKey){
ChangeLanguageKey.visible=false
}
}
}
InputPanel {
id: inputPanel
z: 99
y: parent.height
anchors.left: parent.left
anchors.right: parent.right
states: State {
name: "visible"
when: inputPanel.active
PropertyChanges {
target: inputPanel
y: parent.height - inputPanel.height
}
}
transitions: Transition {
from: ""
to: "visible"
reversible: true
ParallelAnimation {
NumberAnimation {
properties: "y"
duration: 400
easing.type: Easing.InOutBack
}
}
}
CustomComponents.AutoScroller {
id:autoscroller
panelY: inputPanel.y
}
}
This only works in version 5.9 where the objectname property is defined with "changeLanguageKey", for previous versions set the property in the source code and recompile.
No, not without using a custom layout.
You can always modify the layouts that come with the keyboard though.
I was able to hide the hideKeyboard key with this trick. I basically tried to get the reference of the emoji key and thereby was able to disable the next key which is hideKeyboard key.
function disableKey(parent, objectText)
{
var obj = null
if (parent === null)
return null
var children = parent.children
for (var i = 0; i < children.length; i++) {
obj = children[i]
if (obj.text === objectText && obj.toString().substring(0, 7) === "BaseKey") {
console.log("Disabling symbols. " + obj.text)
obj.enabled = false
}
else if(obj.displayText === "HWR"){
console.log("Disabling Handwriting mode button." + obj.displayText + " " + objectText)
obj.visible = false
}
else if(obj.text === ":-)" && obj.toString().substring(0, 7) === "BaseKey"){
console.log("Disabling hidekeyboard key." + obj.text)
children[i+1].visible = false
}
obj = disableKey(obj, objectText)
if (obj)
break
}
return obj
}

Mimedata of dynamic qml object is always empty

I have a treeview from which I drag items to a drop area by creating draggable items dynamically. Everything works except I cannot pass mimedata
function createItem(parentItem)
{
if (itemComponent.status === Component.Ready && draggedItem == null)
{
draggedItem = itemComponent.createObject(
parentItem,
{
"x": positionInParent.x,
"y": positionInParent.y,
"baseColor":Qt.lighter("red", 1.5),
"copyable":false,
visible: true
}
);
draggedItem.Drag.mimeData = { "text/plain": "Hello!" };
draggedItem.Drag.supportedActions = Qt.CopyAction;
draggedItem.Drag.dragType = Drag.Automatic;
draggedItem.Drag.start();
}
else if (itemComponent.status === Component.Error) {
draggedItem = null;
console.log("error creating component");
console.log(itemComponent.errorString());
}
}
when I try to get the mime data from drop event (drop.text field) it is empty.
DropArea
{
height:parent.height
width:parent.width
onDropped: {
drop.acceptProposedAction()
var txt = drop.text; // Empty!!!
console.log("dropped into dest")
}
onEntered: {
console.log("entered dest")
}
}
Resolved it myself! Just to add
draggedItem.Drag.active = true;

Resources