on xenial help sign in
First attempt at dynamic creation of specialized cards
ae928c6Albert Astals
qml/Dash/CardCreator.jsnew file+382
@@ -0,0 +1,382 @@
1+
2+.pragma library
3+
4+/*
5+ * Copyright (C) 2013 Canonical, Ltd.
6+ *
7+ * This program is free software; you can redistribute it and/or modify
8+ * it under the terms of the GNU General Public License as published by
9+ * the Free Software Foundation; version 3.
10+ *
11+ * This program is distributed in the hope that it will be useful,
12+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+ * GNU General Public License for more details.
15+ *
16+ * You should have received a copy of the GNU General Public License
17+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
18+ */
19+
20+.pragma library
21+
22+function createCardComponent(parent, template, components, asynchronous) {
23+ var imports = 'import QtQuick 2.2; \
24+ import Ubuntu.Components 0.1; \
25+ import Ubuntu.Thumbnailer 0.1;';
26+ var code;
27+ code = 'AbstractButton { \
28+ clip: true; \
29+ id: root; \
30+ property var template; \
31+ property var components; \
32+ property var cardData; \
33+ property real fontScale: 1.0; \
34+ property int headerAlignment: Text.AlignLeft; \
35+ property size fixedArtShapeSize: Qt.size(-1, -1); \
36+ readonly property string title: cardData && cardData["title"] || ""; \
37+ property bool asynchronous: ' + asynchronous + '; \
38+ property bool showHeader: true; \
39+ implicitWidth: childrenRect.width; ';
40+
41+ var hasArt = components["art"]["field"];
42+ var hasSummary = components["summary"] || false;
43+ var artAndSummary = hasArt && hasSummary;
44+ var isHorizontal = template["card-layout"] === "horizontal";
45+ var hasBackground = !isHorizontal && (template["card-background"] || components["background"] || artAndSummary);
46+ var inOverlay = hasArt && template && template["overlay"] === true;
47+ var hasTitle = components["title"] || false;
48+ var hasMascot = components["mascot"] || false;
49+ var hasSubtitle = components["subtitle"] || false;
50+ var mascotImageHeight = 'units.gu(5.625)';
51+
52+ if (hasBackground) {
53+ code += 'Loader {\
54+ id: backgroundLoader; \
55+ objectName: "backgroundLoader"; \
56+ anchors.fill: parent; \
57+ asynchronous: root.asynchronous; \
58+ visible: status == Loader.Ready; \
59+ sourceComponent: UbuntuShape { \
60+ objectName: "background"; \
61+ radius: "medium"; \
62+ color: getColor(0) || "white"; \
63+ gradientColor: getColor(1) || color; \
64+ anchors.fill: parent; \
65+ image: backgroundImage.source ? backgroundImage : null; \
66+ property real luminance: 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; \
67+ property Image backgroundImage: Image { \
68+ objectName: "backgroundImage"; \
69+ source: { \
70+ if (cardData && typeof cardData["background"] === "string") return cardData["background"]; \
71+ else if (template && typeof template["card-background"] === "string") return template["card-background"]; \
72+ else return ""; \
73+ } \
74+ } \
75+ function getColor(index) { \
76+ if (cardData && typeof cardData["background"] === "object" \
77+ && (cardData["background"]["type"] === "color" || cardData["background"]["type"] === "gradient")) { \
78+ return cardData["background"]["elements"][index]; \
79+ } else if (template && typeof template["card-background"] === "object" \
80+ && (template["card-background"]["type"] === "color" || template["card-background"]["type"] === "gradient")) { \
81+ return template["card-background"]["elements"][index]; \
82+ } else return undefined; \
83+ } \
84+ } \
85+ }';
86+ }
87+
88+ if (hasArt) {
89+ code += 'readonly property size artShapeSize: artShapeLoader.item ? Qt.size(artShapeLoader.item.width, artShapeLoader.item.height) : Qt.size(-1, -1);'
90+ var imageWidthHeight;
91+ if (isHorizontal) {
92+ if (hasMascot) {
93+ imageWidthHeight = 'width: height * artShape.aspect; \
94+ height: mascotImage.height + 2 * units.gu(1);'
95+ } else {
96+ imageWidthHeight = 'width: height * artShape.aspect; \
97+ height: ' + mascotImageHeight + ' + 2 * units.gu(1);'
98+ }
99+ } else {
100+ imageWidthHeight = 'width: root.width; \
101+ height: width / artShape.aspect;'
102+ }
103+ code += 'Item { \
104+ id: artShapeHolder; \
105+ height: root.fixedArtShapeSize.height != -1 ? root.fixedArtShapeSize.height : artShapeLoader.height; \
106+ width: root.fixedArtShapeSize.width != -1 ? root.fixedArtShapeSize.width : artShapeLoader.width; \
107+ ' + (isHorizontal ? 'anchors.left: parent.left;' : 'anchors.horizontalCenter: parent.horizontalCenter;' ) + '\
108+ Loader { \
109+ id: artShapeLoader; \
110+ objectName: "artShapeLoader"; \
111+ active: cardData && cardData["art"] || false; \
112+ asynchronous: root.asynchronous; \
113+ visible: status == Loader.Ready; \
114+ sourceComponent: UbuntuShape { \
115+ id: artShape; \
116+ radius: "medium"; \
117+ readonly property real aspect: components !== undefined ? components["art"]["aspect-ratio"] : 1; \
118+ readonly property bool aspectSmallerThanImageAspect: aspect < image.aspect; \
119+ Component.onCompleted: updateWidthHeightBindings(); \
120+ onAspectSmallerThanImageAspectChanged: updateWidthHeightBindings(); \
121+ visible: image.status == Image.Ready; \
122+ function updateWidthHeightBindings() { \
123+ if (aspectSmallerThanImageAspect) { \
124+ width = Qt.binding(function() { return !visible ? 0 : image.width }); \
125+ height = Qt.binding(function() { return !visible ? 0 : image.fillMode === Image.PreserveAspectCrop ? image.height : width / image.aspect }); \
126+ } else { \
127+ width = Qt.binding(function() { return !visible ? 0 : image.fillMode === Image.PreserveAspectCrop ? image.width : height * image.aspect }); \
128+ height = Qt.binding(function() { return !visible ? 0 : image.height }); \
129+ } \
130+ } \
131+ image: Image { \
132+ objectName: "artImage"; \
133+ source: cardData && cardData["art"] || ""; \
134+ cache: true; \
135+ asynchronous: root.asynchronous; \
136+ fillMode: components && components["art"]["fill-mode"] === "fit" ? Image.PreserveAspectFit: Image.PreserveAspectCrop; \
137+ readonly property real aspect: implicitWidth / implicitHeight; \
138+ ' + imageWidthHeight + '\
139+ } \
140+ } \
141+ } \
142+ }'
143+ }
144+
145+ if (inOverlay) {
146+ var height;
147+ if (hasMascot) {
148+ var height = 'mascotImage.height + 2 * units.gu(1);';
149+ } else {
150+ var height = mascotImageHeight + ' + 2 * units.gu(1);';
151+ }
152+ code += 'Loader { \
153+ id: overlayLoader; \
154+ anchors { \
155+ left: artShapeHolder.left; \
156+ right: artShapeHolder.right; \
157+ bottom: artShapeHolder.bottom; \
158+ } \
159+ active: artShapeLoader.active && artShapeLoader.item && artShapeLoader.item.image.status === Image.Ready || false; \
160+ asynchronous: root.asynchronous; \
161+ visible: status == Loader.Ready; \
162+ sourceComponent: ShaderEffect { \
163+ id: overlay; \
164+ height: ' + height + ' \
165+ opacity: 0.6; \
166+ property var source: ShaderEffectSource { \
167+ id: shaderSource; \
168+ sourceItem: artShapeLoader.item; \
169+ onVisibleChanged: if (visible) scheduleUpdate(); \
170+ live: false; \
171+ sourceRect: Qt.rect(0, artShapeLoader.height - overlay.height, artShapeLoader.width, overlay.height); \
172+ } \
173+ vertexShader: " \
174+ uniform highp mat4 qt_Matrix; \
175+ attribute highp vec4 qt_Vertex; \
176+ attribute highp vec2 qt_MultiTexCoord0; \
177+ varying highp vec2 coord; \
178+ void main() { \
179+ coord = qt_MultiTexCoord0; \
180+ gl_Position = qt_Matrix * qt_Vertex; \
181+ }"; \
182+ fragmentShader: " \
183+ varying highp vec2 coord; \
184+ uniform sampler2D source; \
185+ uniform lowp float qt_Opacity; \
186+ void main() { \
187+ lowp vec4 tex = texture2D(source, coord); \
188+ gl_FragColor = vec4(0, 0, 0, tex.a) * qt_Opacity; \
189+ }"; \
190+ } \
191+ }';
192+ }
193+
194+ var headerVerticalAnchors;
195+ if (isHorizontal) {
196+ headerVerticalAnchors = 'anchors.top: artShapeHolder.top; \
197+ anchors.topMargin: units.gu(1);';
198+ } else {
199+ if (inOverlay) {
200+ headerVerticalAnchors = 'anchors.bottom: artShapeHolder.bottom; \
201+ anchors.bottomMargin: units.gu(1);';
202+ } else {
203+ headerVerticalAnchors = 'anchors.top: artShapeHolder.bottom; \
204+ anchors.topMargin: units.gu(1);';
205+ }
206+ }
207+
208+
209+ if (hasMascot) {
210+ var useMascotShape = !hasBackground && !inOverlay;
211+ var anchors = "";
212+ if (isHorizontal) {
213+ anchors += 'anchors.left: artShapeHolder.right; \
214+ anchors.leftMargin: units.gu(1);';
215+ } else {
216+ anchors += 'anchors.left: parent.left; \
217+ anchors.leftMargin: units.gu(1);';
218+ }
219+ anchors += headerVerticalAnchors;
220+
221+ if (useMascotShape) {
222+ code += 'Loader { \
223+ id: mascotShapeLoader; \
224+ objectName: "mascotShapeLoader"; \
225+ asynchronous: root.asynchronous; \
226+ active: mascotImage.status === Image.Ready; \
227+ visible: active && status == Loader.Ready; \
228+ width: units.gu(6); \
229+ height: units.gu(5.625); \
230+ sourceComponent: UbuntuShape { image: mascotImage } \
231+ ' + anchors + '\
232+ }';
233+ }
234+
235+ code += 'Image { \
236+ id: mascotImage; \
237+ objectName: "mascotImage"; \
238+ ' + anchors + '\
239+ readonly property int maxSize: Math.max(width, height) * 4; \
240+ source: cardData && cardData["mascot"]; \
241+ width: units.gu(6); \
242+ height: ' + mascotImageHeight + '; \
243+ sourceSize { width: maxSize; height: maxSize } \
244+ fillMode: Image.PreserveAspectCrop; \
245+ horizontalAlignment: Image.AlignHCenter; \
246+ verticalAlignment: Image.AlignVCenter; \
247+ visible:' + (useMascotShape ? 'false' : 'true') + '; \
248+ }';
249+ }
250+
251+ var summaryColorWithBackground = 'backgroundLoader.active && backgroundLoader.item && backgroundLoader.item.luminance < 0.7 ? "white" : "grey"';
252+
253+ if (hasTitle) {
254+ var color;
255+ if (inOverlay) {
256+ color = '"white"';
257+ } else if (hasSummary) {
258+ color = 'summary.color';
259+ } else if (hasBackground) {
260+ color = summaryColorWithBackground;
261+ } else {
262+ color = '"grey"';
263+ }
264+ var anchors = "anchors.right: parent.right;";
265+ if (hasMascot) {
266+ anchors += 'anchors.left: mascotImage.right; \
267+ anchors.leftMargin: units.gu(1);';
268+ } else {
269+ anchors += 'anchors.left: parent.left; \
270+ anchors.leftMargin: units.gu(1);';
271+ }
272+ anchors += headerVerticalAnchors;
273+
274+ var titleAnchors = "";
275+ var subtitleAnchors = "";
276+ if (hasMascot && hasSubtitle) {
277+ titleAnchors = 'anchors { left: parent.left; right: parent.right }';
278+ code += 'Item { \
279+ ' + anchors + '\
280+ height: mascotImage.height; \
281+ Column { \
282+ anchors.verticalCenter: parent.verticalCenter; \
283+ spacing: units.dp(2); \
284+ width: parent.width - x;';
285+ } else {
286+ titleAnchors = anchors;
287+ subtitleAnchors = 'anchors.left: parent.left; \
288+ anchors.leftMargin: units.gu(1); \
289+ anchors.top: titleLabel.bottom; \
290+ anchors.topMargin: units.dp(2);';
291+ }
292+
293+ code += 'Label { \
294+ id: titleLabel; \
295+ objectName: "titleLabel"; \
296+ ' + titleAnchors + '\
297+ elide: Text.ElideRight; \
298+ fontSize: "small"; \
299+ wrapMode: Text.Wrap; \
300+ maximumLineCount: 2; \
301+ font.pixelSize: Math.round(FontUtils.sizeToPixels(fontSize) * fontScale); \
302+ color: ' + color + '; \
303+ text: root.title; \
304+ font.weight: components && components["subtitle"] ? Font.DemiBold : Font.Normal; \
305+ horizontalAlignment: root.headerAlignment; \
306+ }';
307+
308+ if (hasSubtitle) {
309+ code += 'Label { \
310+ id: subtitleLabel; \
311+ objectName: "subtitleLabel"; \
312+ ' + subtitleAnchors + '\
313+ elide: Text.ElideRight; \
314+ fontSize: "small"; \
315+ wrapMode: Text.Wrap; \
316+ maximumLineCount: 2; \
317+ font.pixelSize: Math.round(FontUtils.sizeToPixels(fontSize) * fontScale); \
318+ color: ' + color + '; \
319+ text: cardData && cardData["subtitle"] || " "; \
320+ font.weight: Font.Light; \
321+ horizontalAlignment: root.headerAlignment; \
322+ }';
323+
324+ // Close Column and Item
325+ if (hasMascot)
326+ code += '} }';
327+ }
328+ }
329+
330+ if (hasSummary) {
331+ var summaryTopAnchor;
332+ if (isHorizontal) summaryTopAnchor = "artShapeHolder.bottom";
333+ else if (hasMascot) summaryTopAnchor = "mascotImage.bottom";
334+ else if (hasSubtitle) summaryTopAnchor = "subtitleLabel.bottom";
335+ else if (hasTitle) summaryTopAnchor = "titleLabel.bottom";
336+ else if (hasArt) summaryTopAnchor = "artShapeHolder.bottom";
337+ else summaryTopAnchor = "parent.top";
338+ var color;
339+ if (hasBackground) {
340+ color = summaryColorWithBackground;
341+ } else {
342+ color = '"grey"';
343+ }
344+ code += 'Label { \
345+ id: summary; \
346+ objectName: "summaryLabel"; \
347+ anchors { \
348+ top: ' + summaryTopAnchor + '; \
349+ left: parent.left; \
350+ right: parent.right; \
351+ margins: backgroundLoader.active ? units.gu(1) : 0; \
352+ topMargin: ' + (hasMascot || hasSubtitle ? 'anchors.margins' : 0) + '; \
353+ } \
354+ wrapMode: Text.Wrap; \
355+ maximumLineCount: 5; \
356+ elide: Text.ElideRight; \
357+ text: cardData && cardData["summary"] || ""; \
358+ height: text ? implicitHeight : 0; \
359+ fontSize: "small"; \
360+ color: ' + color + '; \
361+ }';
362+ }
363+
364+ // Close the AbstractButton
365+ if (hasSummary && hasBackground) {
366+ code += 'implicitHeight: summary.y + summary.height + (summary.text && backgroundLoader.active ? units.gu(1) : 0)';
367+ } else if (hasSummary) {
368+ code += 'implicitHeight: summary.y + summary.height';
369+ } else if (hasMascot) {
370+ code += 'implicitHeight: mascotImage.y + mascotImage.height';
371+ } else if (hasTitle) {
372+ code += 'implicitHeight: titleLabel.y + titleLabel.height';
373+ }
374+ code += '}';
375+
376+
377+
378+ code = imports + 'Component {' + code + '}';
379+// console.log(code)
380+ return Qt.createQmlObject(code, parent, "createCardComponent");
381+}
382+
qml/Dash/CardFilterGrid.qml+19−16
@@ -45,25 +45,28 @@ DashRenderer {
45 collapsedRowCount: Math.min(2, cardTool && cardTool.template && cardTool.template["collapsed-rows"] || 2)
45 collapsedRowCount: Math.min(2, cardTool && cardTool.template && cardTool.template["collapsed-rows"] || 2)
46 delegateCreationBegin: genericFilterGrid.delegateCreationBegin
46 delegateCreationBegin: genericFilterGrid.delegateCreationBegin
47 delegateCreationEnd: genericFilterGrid.delegateCreationEnd
47 delegateCreationEnd: genericFilterGrid.delegateCreationEnd
48− delegate: Loader {
48+ delegate: Item {
49− asynchronous: true
50 width: filterGrid.cellWidth
49 width: filterGrid.cellWidth
51 height: filterGrid.cellHeight
50 height: filterGrid.cellHeight
52− Card {
51+ Loader {
53− id: card
52+ sourceComponent: cardTool.cardComponent
54− width: cardTool.cardWidth
55− height: cardTool.cardHeight
56− fixedHeaderHeight: cardTool.headerHeight
57 anchors.horizontalCenter: parent.horizontalCenter
53 anchors.horizontalCenter: parent.horizontalCenter
54+ onLoaded: {
55+// console.log("CardFilterGrid onLoaded", cardTool.artShapeSize, cardTool.cardWidth);
58− objectName: "delegate" + index
56+ item.objectName = "delegate" + index;
57+ item.width = Qt.binding(function() { return cardTool.cardWidth; });
58+ item.height = Qt.binding(function() { return cardTool.cardHeight; });
59+ item.fixedArtShapeSize = Qt.binding(function() { return cardTool.artShapeSize; });
59− cardData: model
60+ item.cardData = Qt.binding(function() { return model; });
60− template: cardTool.template
61+ item.template = Qt.binding(function() { return cardTool.template; });
61− components: cardTool.components
62+ item.components = Qt.binding(function() { return cardTool.components; });
62−
63− headerAlignment: cardTool.headerAlignment
63+ item.headerAlignment = Qt.binding(function() { return cardTool.headerAlignment; });
64−
64+ }
65+ Connections {
66+ target: item
65− onClicked: genericFilterGrid.clicked(index, card.y)
67+ onClicked: genericFilterGrid.clicked(index, item.y)
66− onPressAndHold: genericFilterGrid.pressAndHold(index, card.y)
68+ onPressAndHold: genericFilterGrid.pressAndHold(index, item.y)
69+ }
67 }
70 }
68 }
71 }
69
72
qml/Dash/CardTool.qml+60−22
@@ -15,6 +15,7 @@
15 */
15 */
16
16
17import QtQuick 2.0
17import QtQuick 2.0
18+import "CardCreator.js" as CardCreator
18
19
19/*!
20/*!
20 \brief Tool for introspecting Card properties.
21 \brief Tool for introspecting Card properties.
@@ -65,6 +66,38 @@ Item {
65 return layout;
66 return layout;
66 }
67 }
67
68
69+ property var cardComponent: undefined
70+
71+ onTemplateChanged: {
72+// console.log("onTemplateChanged", cardTool, JSON.stringify(template));
73+ updateCardComponent();
74+ }
75+ onComponentsChanged: {
76+// console.log("onComponentsChanged", cardTool, JSON.stringify(components));
77+ updateCardComponent();
78+ }
79+
80+ // For some reason we get spurious template and components changed signals, since we
81+ // do not want to call createCardComponent without need we cache the stringified vars
82+ // and do a second changed check
83+ property string templateString: ""
84+ property string componentsString: ""
85+
86+ function updateCardComponent()
87+ {
88+ if (cardTool.template === undefined || cardTool.components === undefined)
89+ return;
90+
91+ var tString = JSON.stringify(template);
92+ var cString = JSON.stringify(components);
93+ if (tString != templateString || cString != componentsString) {
94+ templateString = tString;
95+ componentsString = cString;
96+ console.log("CardTool CALLING createCardComponent", cardLoader, tString, cString);
97+ cardComponent = CardCreator.createCardComponent(cardLoader, cardTool.template, cardTool.components, true /* async */);
98+ }
99+ }
100+
68 // FIXME: Saviq
101 // FIXME: Saviq
69 // Only way for the card below to actually be laid out completely.
102 // Only way for the card below to actually be laid out completely.
70 // If invisible or in "data" array, some components are not taken into account.
103 // If invisible or in "data" array, some components are not taken into account.
@@ -108,7 +141,7 @@ Item {
108 if (template["card-size"] >= 12 && template["card-size"] <= 38) return units.gu(template["card-size"]);
141 if (template["card-size"] >= 12 && template["card-size"] <= 38) return units.gu(template["card-size"]);
109 return units.gu(18.5);
142 return units.gu(18.5);
110 case "grid":
143 case "grid":
111− return card.implicitHeight
144+ return cardLoader.item ? cardLoader.item.implicitHeight : 0
112 case "carousel":
145 case "carousel":
113 return cardWidth / (components ? components["art"]["aspect-ratio"] : 1)
146 return cardWidth / (components ? components["art"]["aspect-ratio"] : 1)
114 case undefined:
147 case undefined:
@@ -122,7 +155,7 @@ Item {
122 /*!
155 /*!
123 type:real \brief Height of the card's header.
156 type:real \brief Height of the card's header.
124 */
157 */
125− readonly property alias headerHeight: card.headerHeight
158+ readonly property size artShapeSize: cardLoader.item ? cardLoader.item.artShapeSize : 0
126
159
127 /*!
160 /*!
128 \brief Desired alignment of header components.
161 \brief Desired alignment of header components.
@@ -161,15 +194,8 @@ Item {
161 }
194 }
162 }
195 }
163
196
164− Card {
197+ Loader {
165− id: card
198+ id: cardLoader
166− objectName: "cardToolCard"
167− template: cardTool.template
168− components: cardTool.components
169−
170− width: cardTool.cardWidth || implicitWidth
171− height: cardTool.cardHeight || implicitHeight
172−
173 property var fields: ["art", "mascot", "title", "subtitle", "summary"]
199 property var fields: ["art", "mascot", "title", "subtitle", "summary"]
174 property var maxData: {
200 property var maxData: {
175 "art": Qt.resolvedUrl("graphics/checkers.png"),
201 "art": Qt.resolvedUrl("graphics/checkers.png"),
@@ -178,19 +204,31 @@ Item {
178 "subtitle": "—",
204 "subtitle": "—",
179 "summary": "—\n—\n—\n—\n—"
205 "summary": "—\n—\n—\n—\n—"
180 }
206 }
181−
207+ sourceComponent: cardTool.cardComponent
182− onComponentsChanged: {
208+ onLoaded: {
209+// console.log("CardTool onLoaded");
210+ item.asynchronous = false;
211+ item.template = Qt.binding(function() { return cardTool.template; });
212+ item.components = Qt.binding(function() { return cardTool.components; });
213+ item.width = Qt.binding(function() { return cardTool.cardWidth || item.implicitWidth; });
214+ item.height = Qt.binding(function() { return cardTool.cardHeight || item.implicitHeight; });
215+ }
216+ Connections {
217+ target: cardLoader.item
218+ onComponentsChanged: {
219+// console.log("CardTool onComponentsChanged");
183− var data = {};
220+ var data = {};
184− for (var k in fields) {
221+ for (var k in cardLoader.fields) {
185− var component = components[fields[k]];
222+ var component = cardLoader.item.components[cardLoader.fields[k]];
186− var key = fields[k];
223+ var key = cardLoader.fields[k];
187− if ((typeof component === "string" && component.length > 0) ||
224+ if ((typeof component === "string" && component.length > 0) ||
188− (typeof component === "object" && component !== null
225+ (typeof component === "object" && component !== null
189− && typeof component["field"] === "string" && component["field"].length > 0)) {
226+ && typeof component["field"] === "string" && component["field"].length > 0)) {
190− data[key] = maxData[key];
227+ data[key] = cardLoader.maxData[key];
228+ }
191 }
229 }
230+ cardLoader.item.cardData = data;
192 }
231 }
193− cardData = data;
194 }
232 }
195 }
233 }
196}
234}
qml/Dash/GenericScopeView.qml+3
@@ -131,7 +131,9 @@ FocusScope {
131
131
132 count: results.count
132 count: results.count
133 template: model.renderer
133 template: model.renderer
134+// onTemplateChanged: console.log("TCH", cardTool, JSON.stringify(template))
134 components: model.components
135 components: model.components
136+// onComponentsChanged: console.log("CCH", cardTool, JSON.stringify(components))
135 viewWidth: parent.width
137 viewWidth: parent.width
136 }
138 }
137
139
@@ -174,6 +176,7 @@ FocusScope {
174 }
176 }
175 updateDelegateCreationRange();
177 updateDelegateCreationRange();
176 item.cardTool = cardTool;
178 item.cardTool = cardTool;
179+ console.log("rendererLoader onLoaded");
177 }
180 }
178
181
179 Component.onDestruction: {
182 Component.onDestruction: {