Invalid grouped property access - qt

In this qml code:
Component {
id: userdelegate
PictureBox {
...
icon: model.icon
icon.heigth: 50
}
}
PictureBox comes from the PictureBox.qml system file in this way:
...
Image {
id: icon
...
width: parent.width; height: 150
}
Running qml, I have the error in the title.
I need to use PictureBox.qml, but I can't change it.
How can I override default height value for PictureBox.qml icon?

You can try to bypass QML's scoping rules by traversing Item children until you can find the Image and manipulate it directly. It's possible it could break in the future, but item.toString() gives you something useful:
item.toString() -> "QQuickImage(0x114054350)"
So, you can try something like this (not tested):
function findItemOfType(type, item) {
if (item.toString().indexOf(type) != -1) {
return child;
}
for (var i=0;i < children.length;++i) {
var child = children[i];
var result = findItemOfType(type, child.children);
if (result != null) {
return result;
}
}
return null;
}
Using it like this:
findItemOfType("QQuickImage", pictureBoxId);

Related

SwiftUI - share dictionary among views, unclear what arguments to use at #Main / WindowGroup

I'm trying to build an app (macOS, but would be the same for iOS) that creates a number of grids, the outcome of which is to be shown in a second screen. For this, I'm sharing data across these screens, and I'm running into an issue here, I hope someone can help or point me in the right direction. I'll share a simplified version of the code below (working in Xcode 14.0.1)
The code creates a dictionary that can be shown in a grid, on which calculations can be done. The idea is then to add this grid, with some descriptive variables, into another dictionary
The building blocks of the grid are cells
Import Foundation
struct Cell: Comparable, Equatable, Identifiable, Hashable {
static func == (lhs: Cell, rhs: Cell) -> Bool {
lhs.randomVarOne == rhs.randomVarOne
}
var randomVarOne: Double
var randomVarTwo: Bool
// other vars omitted here
var id: Int { randomVarOne }
static func < (lhs: Cell, rhs: Cell) -> Bool {
return lhs.randomVarOne < rhs.randomVarOne
}
}
this is also where there are a bunch of funcs to calculate next neighbor cells in the grid etc
then the grid is defined in a class:
class Info: ObservableObject, Hashable {
static func == (lhs: Info, rhs: Info) -> Bool {
lhs.grid == rhs.grid
}
func hash(into hasher: inout Hasher) {
hasher.combine(grid)
}
#Published var grid = [Cell]()
var arrayTotal = 900
#Published var toBeUsedForTheGridCalculations: Double = 0.0
var toBeUsedToSetTheVarAbove: Double = 0.0
var rowTotalDouble: Double {sqrt(Double(arrayTotal)) }
var rowTotal: Int {
Int(rowTotalDouble) != 0 ? Int(rowTotalDouble) : 10 }
The class includes a func to create and populate the grid with Cells and add these Cells to the grid var. It also includes the formulas to do the calculations on the grid using a user input. The class did not seem to need an initializer.
This is the Scenario struct:
struct Scenario: Comparable, Equatable, Identifiable, Hashable {
static func == (lhs: Scenario, rhs: Scenario) -> Bool {
lhs.scenarioNumber == rhs.scenarioNumber
}
func hash(into hasher: inout Hasher) {
hasher.combine(scenarioNumber)
}
var scenarioNumber: Int
var date: Date
var thisIsOneSnapshot = [Info]()
var id: Int { scenarioNumber }
static func < (lhs: Scenario, rhs: Scenario) -> Bool {
return lhs.scenarioNumber < rhs.scenarioNumber
}
}
added hashable since it uses the Info class as an input.
Then there is the class showing the output overview
class OutputOverview: ObservableObject {
#Published var snapshot = [Scenario]()
// the class includes a formula of how to add the collection of cells (grid) and the additional variables to the snapshot dictionary. Again no initializer was necessary.
Now to go to the ContentView.
struct ContentView: View {
#Environment(\.openURL) var openURL
var scenarioNumberInput: Int = 0
var timeStampAssigned: Date = Date.now
#ObservedObject private var currentGrid: Info = Info()
#ObservedObject private var scenarios: Combinations = Combinations()
var usedForTheCalculations: Double = 0.0
var rows =
[
GridItem(.flexible()),
// whole list of GridItems, I do not know how to calculate these:
// var rows = Array(repeating: GridItem(.flexible()), count: currentGrid.rowTotal)
//gives error "Cannot use instance member 'currentGrid' within property initializer;
// property iunitializers run before 'self' is available
]
var body: some View {
GeometryReader { geometry in
VStack {
ScrollView {
LazyHGrid(rows: rows, spacing: 0) {
ForEach(0..<currentGrid.grid.count, id :\.self) { w in
let temp = currentGrid.grid[w].varThatAffectsFontColor
let temp2 = currentGrid.grid[w].varThatAffectsBackground
Text("\(currentGrid.grid[w].randomVarOne, specifier: "%.2f")")
.frame(width: 25, height: 25)
.border(.black)
.font(.system(size: 7))
.foregroundColor(Color(wordName: temp))
.background(Color(wordName: temp2))
}
}
.padding(.top)
}
VStack{
HStack {
Button("Start") {
}
// then some buttons to do the calculations
Button("Add to collection"){
scenarios.addScenario(numbering: scenarioNumberInput, timeStamp:
Date.now, collection: currentGrid.grid)
} // this should add the newly recalculated grid to the dictionary
Button("Go to Results") {
guard let url = URL(string: "myapp://scenario") else { return }
openURL(url)
} // to go to the screen showing the scenarios
Then the second View, the ScenarioView:
struct ScenarioView: View {
#State var selectedScenario = 1
#ObservedObject private var scenarios: OutputOverview
var pickerNumbers = [ 1, 2, 3, 4 , 5]
// this is to be linked to the number of scenarios completed,this code is not done yet.
var rows =
[
GridItem(.flexible()),
GridItem(.flexible()),
// similar list of GridItems here....
var body: some View {
Form {
Section {
Picker("Select a scenario", selection: $selectedScenario) {
ForEach(pickerNumbers, id: \.self) {
Text("\($0)")
}
}
}
Section {
ScrollView {
if let idx = scenarios.snapshot.firstIndex(where:
{$0.scenarioNumber == selectedScenario}) {
LazyHGrid(rows: rows, spacing: 0) {
ForEach(0..<scenarios.snapshot[idx].thisIsOneSnapshot.count,
id :\.self) { w in
let temp =
scenarios.snapshot[idx].thisIsOneSnapshot[w].varThatAffectsFontColor
let temp2 =
scenarios.snapshot[idx].thisIsOneSnapshot[w].varThatAffectsBackground
Text("\(scenarios.snapshot[idx].thisIsOneSnapshot[w].randomVarOne, specifier: "%.2f")")
.frame(width: 25, height: 25)
.border(.black)
.font(.system(size: 7))
.foregroundColor(Color(wordName: temp))
.background(Color(wordName: temp2))
}
}
}
}
}
}
}
}
Now while the above does not (for the moment..) give me error messages, I am not able to run the PreviewProvider in the second View. The main problem is in #main:
import SwiftUI
#main
struct ThisIsTheNameOfMyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.handlesExternalEvents(matching: ["main"])
WindowGroup("Scenarios") {
ScenarioView()
// error messages here: 'ScenarioView' initializer is inaccessible due to "private"
// protection level - I don't know what is set to private in ScenarioView that could
// cause this
// second error message: missing argument for parameter 'scenarios' in call
}
.handlesExternalEvents(matching: ["scenario"])
}
}
I am at a loss on how to solve these 2 error messages and would be very grateful for any tips or guidance. Apologies if this question is very long, I scanned many other forum questions and could not find any good answers.
I have tried adding pro forma data in #main as follows
#main
struct FloodModelScenarioViewerApp: App {
#State var scenarios = Scenario(scenarioNumber: 1, date: Date.now)
var body: some Scene {
WindowGroup {
ContentView()
}
.handlesExternalEvents(matching: ["main"])
WindowGroup("Scenarios") {
ScenarioView(scenarios: scenarios)
}
.handlesExternalEvents(matching: ["scenario"])
}
}
This still gives 2 error messages:
same issue with regards to ScenarioView initialiser being inaccessible due to being 'private'
Cannot convert value of type 'Scenario' to expected argument type 'OutputOverview'
Just remove the private from
#ObservedObject private var scenarios: OutputOverview
The value is coming from he parent so the parent needs access. So put
#StateObject private var scenarios: OutputOverview = .init()
in FloodModelScenarioViewerApp
#StateObject is for initializing ObservableObjects and #ObservedObject is for passing them around.
I don't know if your code will work after you read this question, and that's because there are many things to correct, but you can start with these:
In Cell, you shouldn't use an id that is a variable, this may cause inconsistent behavior. Use something like:
let id = UUID()
When you initialize ContentView, you can't use currentGrid inside a variable because currentGrid will not be available before all variables are initialized. Meaning, you are trying to initialize rows before currentGrid actually exists. You can try using the .onAppear modifier:
var rows = [GridItem]()
var body: some View {
GeometryReader { geometry in
// ... view code in here
}
.onAppear {
var rows = Array(repeating: GridItem(.flexible()), count: currentGrid.rowTotal)
}
}
This creates the view and, before showing it, the grid is set to its proper value.
The message 'ScenarioView' initializer is inaccessible due to "private" protection level seems clear: you must provide a value to to the variable scenarios (it doesn't have a default value) but it's marked as private. Remove private.
#ObservedObject var scenarios: OutputOverview
Then, remember to pass a value of type OutputOverview for the variable when you call the view:
ScenarioView(scenarios: aVariableOfTypeOutputOverview)
The type mismatch error you get inside the #main code is also clear - you have defined a variable of type Scenario:
#State var scenarios = Scenario(scenarioNumber: 1, date: Date.now)
but ScenarioView requires another type:
#ObservedObject private var scenarios: OutputOverview
One of them needs change for your code to work.

Getting SwiftUI wrapper of AVPlayer to pause when view disappears

TL;DR
Can't seem to use binding to tell wrapped AVPlayer to stop — why not? The "one weird trick" from Vlad works for me, without state & binding, but why?
See Also
My question is something like this one but that poster wanted to wrap an AVPlayerViewController and I want to control playback programmatically.
This guy also wondered when updateUIView() was called.
What happens (Console logs shown below.)
With code as shown here,
The user taps "Go to Movie"
MovieView appears and the vid plays
This is because updateUIView(_:context:) is being called
The user taps "Go back Home"
HomeView reappears
Playback halts
Again updateUIView is being called.
See Console Log 1
But... remove the ### line, and
Playback continues even when the home view returns
updateUIView is called on arrival but not departure
See Console log 2
If you uncomment the %%% code (and comment out what precedes it)
You get code I thought was logically and idiomatically correct SwiftUI...
...but "it doesn't work". I.e. the vid plays on arrival but continues on departure.
See Console log 3
The code
I do use an #EnvironmentObject so there is some sharing of state going on.
Main content view (nothing controversial here):
struct HomeView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
ZStack() { // +++ Weird trick ### fails if this is Group(). Wtf?
if router.page == .home {
Button(action: { self.router.page = .movie }) {
Text("Go to Movie")
}
} else if router.page == .movie {
MovieView()
}
}
}
}
which uses one of these (still routine declarative SwiftUI):
struct MovieView: View {
#EnvironmentObject var router: ViewRouter
// #State private var isPlaying: Bool = false // %%%
var body: some View {
VStack() {
PlayerView()
// PlayerView(isPlaying: $isPlaying) // %%%
Button(action: { self.router.page = .home }) {
Text("Go back Home")
}
}.onAppear {
print("> onAppear()")
self.router.isPlayingAV = true
// self.isPlaying = true // %%%
print("< onAppear()")
}.onDisappear {
print("> onDisappear()")
self.router.isPlayingAV = false
// self.isPlaying = false // %%%
print("< onDisappear()")
}
}
}
Now we get into the AVKit-specific stuff. I use the approach described by Chris Mash.
The aforementioned PlayerView, the wrappER:
struct PlayerView: UIViewRepresentable {
#EnvironmentObject var router: ViewRouter
// #Binding var isPlaying: Bool // %%%
private var myUrl : URL? { Bundle.main.url(forResource: "myVid", withExtension: "mp4") }
func makeUIView(context: Context) -> PlayerView {
PlayerUIView(frame: .zero , url : myUrl)
}
// ### This one weird trick makes OS call updateUIView when view is disappearing.
class DummyClass { } ; let x = DummyClass()
func updateUIView(_ v: PlayerView, context: UIViewRepresentableContext<PlayerView>) {
print("> updateUIView()")
print(" router.isPlayingAV = \(router.isPlayingAV)")
// print(" isPlaying = \(isPlaying)") // %%%
// This does work. But *only* with the Dummy code ### included.
// See also +++ comment in HomeView
if router.isPlayingAV { v.player?.pause() }
else { v.player?.play() }
// This logic looks reversed, but is correct.
// If it's the other way around, vid never plays. Try it!
// if isPlaying { v?.player?.play() } // %%%
// else { v?.player?.pause() } // %%%
print("< updateUIView()")
}
}
And the wrappED UIView:
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
var player: AVPlayer?
init(frame: CGRect, url: URL?) {
super.init(frame: frame)
guard let u = url else { return }
self.player = AVPlayer(url: u)
self.playerLayer.player = player
self.layer.addSublayer(playerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
required init?(coder: NSCoder) { fatalError("not implemented") }
}
And of course the view router, based on the Blckbirds example
class ViewRouter : ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter, Never>()
enum Page { case home, movie }
var page = Page.home { didSet { objectWillChange.send(self) } }
// Claim: App will never play more than one vid at a time.
var isPlayingAV = false // No didSet necessary.
}
Console Logs
Console log 1 (playing stops as desired)
> updateUIView() // First call
router.isPlayingAV = false // Vid is not playing => play it.
< updateUIView()
> onAppear()
< onAppear()
> updateUIView() // Second call
router.isPlayingAV = true // Vid is playing => pause it.
< updateUIView()
> onDisappear() // After the fact, we clear
< onDisappear() // the isPlayingAV flag.
Console log 2 (weird trick disabled; playing continues)
> updateUIView() // First call
router.isPlayingAV = false
< updateUIView()
> onAppear()
< onAppear()
// No second call.
> onDisappear()
< onDisappear()
Console log 3 (attempt to use state & binding; playing continues)
> updateUIView()
isPlaying = false
< updateUIView()
> onAppear()
< onAppear()
> updateUIView()
isPlaying = true
< updateUIView()
> updateUIView()
isPlaying = true
< updateUIView()
> onDisappear()
< onDisappear()
Well... on
}.onDisappear {
print("> onDisappear()")
self.router.isPlayingAV = false
print("< onDisappear()")
}
this is called after view is removed (it is like didRemoveFromSuperview, not will...), so I don't see anything bad/wrong/unexpected in that subviews (or even it itself) is not updated (in this case updateUIView)... I would rather surprise if it would be so (why update view, which is not in view hierarchy?!).
So this
class DummyClass { } ; let x = DummyClass()
is rather some wild bug, or ... bug. Forget about it and never use such stuff in releasing products.
OK, one would now ask, how to do with this? The main issue I see here is design-originated, specifically tight-coupling of model and view in PlayerUIView and, as a result, impossibility to manage workflow. AVPlayer here is not part of view - it is model and depending on its states AVPlayerLayer draws content. Thus the solution is to tear apart those entities and manage separately: views by views, models by models.
Here is a demo of modified & simplified approach, which behaves as expected (w/o weird stuff and w/o Group/ZStack limitations), and it can be easily extended or improved (in model/viewmodel layer)
Tested with Xcode 11.2 / iOS 13.2
Complete module code (can be copy-pasted in ContentView.swift in project from template)
import SwiftUI
import Combine
import AVKit
struct MovieView: View {
#EnvironmentObject var router: ViewRouter
// just for demo, but can be interchangable/modifiable
let playerModel = PlayerViewModel(url: Bundle.main.url(forResource: "myVid", withExtension: "mp4")!)
var body: some View {
VStack() {
PlayerView(viewModel: playerModel)
Button(action: { self.router.page = .home }) {
Text("Go back Home")
}
}.onAppear {
self.playerModel.player?.play() // << changes state of player, ie model
}.onDisappear {
self.playerModel.player?.pause() // << changes state of player, ie model
}
}
}
class PlayerViewModel: ObservableObject {
#Published var player: AVPlayer? // can be changable depending on modified URL, etc.
init(url: URL) {
self.player = AVPlayer(url: url)
}
}
struct PlayerView: UIViewRepresentable { // just thing wrapper, as intended
var viewModel: PlayerViewModel
func makeUIView(context: Context) -> PlayerUIView {
PlayerUIView(frame: .zero , player: viewModel.player) // if needed viewModel can be passed completely
}
func updateUIView(_ v: PlayerUIView, context: UIViewRepresentableContext<PlayerView>) {
}
}
class ViewRouter : ObservableObject {
enum Page { case home, movie }
#Published var page = Page.home // used native publisher
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
var player: AVPlayer?
init(frame: CGRect, player: AVPlayer?) { // player is a model so inject it here
super.init(frame: frame)
self.player = player
self.playerLayer.player = player
self.layer.addSublayer(playerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
required init?(coder: NSCoder) { fatalError("not implemented") }
}
struct ContentView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
Group {
if router.page == .home {
Button(action: { self.router.page = .movie }) {
Text("Go to Movie")
}
} else if router.page == .movie {
MovieView()
}
}
}
}

Qt3d Sceneloader: Replace the material component of an Entity with my custom material at runtime

I have the following code to load a collada scene using the SceneLoader:
SceneLoader{
id: sceneLoader
source: "file:///home/rui/projects/cad/bodyplacement_lm36_v2.dae"
MetalRoughMaterial {
id:metal_mat
objectName: "MetalRoughMaterial"
metalness: 0
roughness: 0.9
}
onStatusChanged: {
console.log("SceneLoader status: " + status);
if (status == SceneLoader.Ready) {
console.log("Scene is ready");
var entitynames=sceneLoader.entityNames();
for (var i = 0; i < entitynames.length; ++i) {
var entityname=entitynames[i];
var entityvar=sceneLoader.entity(entityname);
for (var j = 0; j< entityvar.components.length; ++j) {
var cmp=entityvar.components[j]
if(cmp){
var cmp_class=cmp.toString();
if(cmp_class.indexOf("QPhongMaterial")>=0){
entityvar.components[j]=metal_mat;
}
}
}
}
}
}
}
As stated in docs (https://doc.qt.io/qt-5/qt3drender-qsceneloader.html#details):
The loader will try to determine the best material to be used based on the properties of the model file. If you wish to use a custom material, you will have to traverse the tree and replace the default associated materials with yours.
After I iterate all the entities I try to replace the material component with the code:
entityvar.components[j]=metal_mat;
but it isn't working. After debugging I can see that the loaded material isn't replaced.
How can I replace the material component with my custom material at runtime?
I have been working to solve this issue. Your code has a couple of issues.
entityvar.components[j]=metal_mat;
this will not work because qml wants you to replace the whole array, you cannot modify it in place
var entitynames=sceneLoader.entityNames();
for (var i = 0; i < entitynames.length; ++i) {
var entityname=entitynames[i];
var entityvar=sceneLoader.entity(entityname);
This will not work for files that have entities or submodels with identical names, such as no name at all.
Here is how I solved these issues:
SceneLoader loads the scene from the fbx or obj file and places it as childNodes into its parent. So the only way to traverse the tree is to look at the parent entity's childNodes array.
import Qt3D.Render 2.12
import Qt3D.Extras 2.12
import Qt3D.Core 2.12
Entity {
id: rootEntity
property alias source: loader.source
property Transform transform: Transform{}
components:[ loader, transform ]
SceneLoader {
id: loader
Component {
id: pbrMatTemplate
MetalRoughMaterial { }
}
onStatusChanged: {
if(status == SceneLoader.Ready) {
instantiateMaterials();
}
}
}
function instantiateMaterials() {
// TODO create as many materials as you need here
var newmat = pbrMatTemplate.createObject(loader);
function traverseNodes(node) {
if(node.components) {
var comps = [];
for (var j = 0; j< node.components.length; ++j) {
var cmp = node.components[j];
var cmp_class = cmp.toString();
if(cmp_class.indexOf("QPhongMaterial")>=0) {
// TODO: look up material from list of materials you created instead of just using one
comps.push(newMat);
} else {
comps.push(cmp);
}
}
// replace whole list of components
node.components = comps;
}
if(node.childNodes) {
for(var i=0; i<node.childNodes.length; i++) {
if(node.childNodes[i] == null) continue;
traverseNodes(node.childNodes[i]);
}
}
} // traverseNodes()
traverseNodes(rootEntity);
}
}

Angular2 model binding not working

I try to update an input value in Angular 2, it works the first time the size value exceeds maxSize, but afterwords it does not work anymore. It seems like when I am setting this.size to some value the UI is not updated, am I overlooking something ?
HTML:
<input type="text" class="form-control" [value]="size" (input)="size = updateValue($event)">
Code:
export class BrushSizePicker {
#Input() minValue;
#Input() maxValue;
#Input() size;
increaseValue(event) {
this.size++;
this.checkValue();
}
decreaseValue(event) {
this.size--;
this.checkValue();
}
updateValue(event) {
this.size = parseInt(event.target.value);
this.checkValue();
return this.size;
}
private checkValue() {
if (this.size > this.maxValue) {
this.size = this.maxValue;
}
if (this.size < this.minValue) {
this.size = this.minValue;
}
}
EDIT:
I logged what happened: checkValue is called every time with the correct input, and it returns the correct value. But the new value is not set into the input field / value field
While it may not solve the problem, the way you have implemented the input event can be simplified. I would have written it like this, side-effect free functions:
updateValue(event) { // The method name with this change is a misnomer
return this.checkValue(parseInt(event.target.value));
}
private checkValue(item) {
if (item > this.maxValue) {
return this.maxValue;
}
else if (else < this.minValue) {
return this.maxValue;
}
return item;
}

Getting around list<> properties being readonly

I have two custom components written in QML
//XOption.qml
Container {
id: xOption
property string title;
property bool active: false;
function makeActive() {active=true}
onActiveChanged {
//alter appearance of option to reflect whether active/not
}
onTouch {
if (touchEvent reflects a tap) {
//notify underlying c++ engine that I was tapped
//engine will notify parent control that I was tapped by setting the flag var
}
}
//label to display title and some other visual components
}
//XParent.qml
Container {
id: XParent;
property list<XOption> options;
property int selectedOption: 0;
property string flag: cppengine.flag;
onCreationCompleted {
for (var k = 0; k < children.length; ++k) {
if (k==selectedOption)
options[k].makeActive()
}
cppengine.declareParentage(options);
}
onFlagChanged {
if (flag indicates that one of my child options was tapped) {
//determine from flag which option was tapped
tappedOption.makeActive()
//make other options inactive
}
}
}
But now I want to use XParent in another QML document and assign any number of different XOptions to it like so:
Container {
XParent {
options: [
XOption {
title: "title1";
},
XOption {
title: "title2";
}
]
}
}
However, when doing so, I get the error:
Invalid property assignment: "options" is a read-only property
Is there any way I could get around this? I've tried making options a string array type variant, that would contain the title of every child option to create, and then adding a ComponentDefinition for XOption to XParent and creating one for every title that was specified, but if I do that I am unable to call XOption's makeActive(), which is absolutely necessary.

Resources