on xenial help sign in
Merge card-tool
0bf39e2Michał Sawicz
qml/Dash/Card.qml+6−13
@@ -23,20 +23,12 @@ AbstractButton {
23 property var components
23 property var components
24 property var cardData
24 property var cardData
25
25
26− width: {
26+ implicitWidth: childrenRect.width
27− if (template !== undefined) {
28− if (template["card-layout"] === "horizontal") return units.gu(38);
29− switch (template['card-size']) {
30− case "small": return units.gu(12);
31− case "large": return units.gu(38);
32− }
33− }
34− return units.gu(18.5);
35− }
36− height: childrenRect.height
27+ implicitHeight: childrenRect.height
37
28
38 UbuntuShape {
29 UbuntuShape {
39 id: artShape
30 id: artShape
31+ radius: "medium"
40 objectName: "artShape"
32 objectName: "artShape"
41 width: {
33 width: {
42 if (!visible) return 0
34 if (!visible) return 0
@@ -60,7 +52,7 @@ AbstractButton {
60 // FIXME uncomment when having investigated / fixed the crash
52 // FIXME uncomment when having investigated / fixed the crash
61 //sourceSize.width: width > height ? width : 0
53 //sourceSize.width: width > height ? width : 0
62 //sourceSize.height: height > width ? height : 0
54 //sourceSize.height: height > width ? height : 0
63− fillMode: components["art"]["fill-mode"] === "fit" ? Image.PreserveAspectFit: Image.PreserveAspectCrop
55+ fillMode: components && components["art"]["fill-mode"] === "fit" ? Image.PreserveAspectFit: Image.PreserveAspectCrop
64
56
65 property real aspect: implicitWidth / implicitHeight
57 property real aspect: implicitWidth / implicitHeight
66 }
58 }
@@ -82,11 +74,12 @@ AbstractButton {
82
74
83 Label {
75 Label {
84 objectName: "summaryLabel"
76 objectName: "summaryLabel"
85− anchors { top: header.bottom; left: parent.left; right: parent.right }
77+ anchors { top: header.visible ? header.bottom : artShape.bottom; left: parent.left; right: parent.right }
86 wrapMode: Text.Wrap
78 wrapMode: Text.Wrap
87 maximumLineCount: 5
79 maximumLineCount: 5
88 elide: Text.ElideRight
80 elide: Text.ElideRight
89 text: cardData && cardData["summary"] || ""
81 text: cardData && cardData["summary"] || ""
90 height: text ? implicitHeight : 0
82 height: text ? implicitHeight : 0
83+ fontSize: "small"
91 }
84 }
92}
85}
qml/Dash/CardHeader.qml+14−3
@@ -27,14 +27,21 @@ Item {
27 property alias altPrice: altPriceLabel.text
27 property alias altPrice: altPriceLabel.text
28
28
29 visible: mascotImage.status === Image.Ready || title || price
29 visible: mascotImage.status === Image.Ready || title || price
30− height: row.height > 0 ? row.height + row.spacing * 2 : 0
30+ height: row.height > 0 ? row.height + row.margins * 2 : 0
31
31
32 Row {
32 Row {
33 id: row
33 id: row
34 objectName: "outerRow"
34 objectName: "outerRow"
35
35
36− anchors { top: parent.top; left: parent.left; right: parent.right; margins: spacing }
36+ property real margins: units.gu(1)
37− spacing: units.gu(1)
37+
38+ anchors {
39+ top: parent.top; left: parent.left; right: parent.right
40+ margins: margins
41+ leftMargin: spacing
42+ rightMargin: spacing
43+ }
44+ spacing: mascotShape.visible ? margins : 0
38
45
39 UbuntuShape {
46 UbuntuShape {
40 id: mascotShape
47 id: mascotShape
@@ -53,6 +60,7 @@ Item {
53 Column {
60 Column {
54 objectName: "column"
61 objectName: "column"
55 width: parent.width - x
62 width: parent.width - x
63+ spacing: units.gu(0.5)
56
64
57 Label {
65 Label {
58 id: titleLabel
66 id: titleLabel
@@ -62,6 +70,7 @@ Item {
62 font.weight: Font.DemiBold
70 font.weight: Font.DemiBold
63 wrapMode: Text.Wrap
71 wrapMode: Text.Wrap
64 maximumLineCount: 2
72 maximumLineCount: 2
73+ fontSize: "small"
65 }
74 }
66
75
67 Label {
76 Label {
@@ -69,7 +78,9 @@ Item {
69 objectName: "subtitleLabel"
78 objectName: "subtitleLabel"
70 anchors { left: parent.left; right: parent.right }
79 anchors { left: parent.left; right: parent.right }
71 elide: Text.ElideRight
80 elide: Text.ElideRight
81+ font.weight: Font.Light
72 visible: titleLabel.text && text
82 visible: titleLabel.text && text
83+ fontSize: "x-small"
73 }
84 }
74
85
75 Row {
86 Row {
qml/Dash/CardTool.qmlnew file+136
@@ -0,0 +1,136 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+
19+/*!
20+ \brief Tool for introspecting Card properties.
21+
22+ Some properties of Cards we need to determine category-wide (like card sizes in grid),
23+ so we should not do them per-Card but in the category renderer.
24+
25+ This component creates an invisible card filled with maximum mapped data and calculates
26+ or measures card properties for this configuration.
27+ */
28+
29+Item {
30+ id: cardTool
31+ /*!
32+ Template supplied for the category.
33+ */
34+ property var template
35+
36+ /*!
37+ Component mapping supplied for the category.
38+ */
39+ property var components
40+
41+ /*!
42+ Width of the view, based on which carousel width is calculated.
43+ */
44+ property var viewWidth
45+
46+ // FIXME: Saviq
47+ // Only way for the card below to actually be laid out completely.
48+ // If invisible or in "data" array, some components are not taken into account.
49+ width: 0
50+ height: 0
51+ clip: true
52+
53+ /*!
54+ type:real \brief Width to be enforced on the card in this configuration.
55+
56+ If undefined, should use implicit width of the actual card.
57+ */
58+ property var cardWidth: {
59+ switch (template !== undefined && template["category-layout"]) {
60+ case "grid":
61+ case "vertical-journal":
62+ if (template["card-layout"] === "horizontal") return units.gu(38);
63+ switch (template["card-size"]) {
64+ case "small": return units.gu(12);
65+ case "large": return units.gu(38);
66+ }
67+ return units.gu(18.5);
68+ case "carousel":
69+ if (viewWidth === undefined) return undefined
70+ if (viewWidth <= units.gu(40)) return units.gu(18)
71+ if (viewWidth >= units.gu(128)) return units.gu(26)
72+ return units.gu(18 + Math.round((viewWidth - units.gu(40))/ units.gu(11)))
73+ case undefined:
74+ case "organic-grid":
75+ case "journal":
76+ default:
77+ return undefined;
78+ }
79+ }
80+
81+ /*!
82+ type:real \brief Height to be enforced on the card in this configuration.
83+
84+ If undefined, should use implicit height of the actual card.
85+ */
86+ property var cardHeight: {
87+ switch (template !== undefined && template["category-layout"]) {
88+ case "journal":
89+ if (template["card-size"] >= 12 && template["card-size"] <= 38) return units.gu(template["card-size"]);
90+ return units.gu(18.5);
91+ case "grid":
92+ return card.implicitHeight
93+ case "carousel":
94+ return cardWidth / (components ? components["art"]["aspect-ratio"] : 1)
95+ case undefined:
96+ case "organic-grid":
97+ case "vertical-journal":
98+ default:
99+ return undefined;
100+ }
101+ }
102+
103+ property QtObject priv: card
104+
105+ Card {
106+ id: card
107+ template: cardTool.template
108+ components: cardTool.components
109+
110+ width: cardTool.cardWidth || implicitWidth
111+ height: cardTool.cardHeight || implicitHeight
112+
113+ property var fields: ["art", "mascot", "title", "subtitle", "summary"]
114+ property var maxData: {
115+ "art": Qt.resolvedUrl("graphics/checkers.png"),
116+ "mascot": Qt.resolvedUrl("graphics/checkers.png"),
117+ "title": "—\n—",
118+ "subtitle": "—",
119+ "summary": "—\n—\n—\n—\n—"
120+ }
121+
122+ onComponentsChanged: {
123+ var data = {};
124+ for (var k in fields) {
125+ var component = components[fields[k]];
126+ var key = fields[k];
127+ if ((typeof component === "string" && component.length > 0) ||
128+ (typeof component === "object" && component !== null
129+ && typeof component["field"] === "string" && component["field"].length > 0)) {
130+ data[key] = maxData[key];
131+ }
132+ }
133+ cardData = data;
134+ }
135+ }
136+}
qml/Dash/graphics/checkers.pngnew file

Binary file not shown.

tests/qmltests/CMakeLists.txt+1
@@ -44,6 +44,7 @@ add_qml_test(Dash DashPreview)
44add_qml_test(Dash GenericPreview)
44add_qml_test(Dash GenericPreview)
45add_qml_test(Dash Card)
45add_qml_test(Dash Card)
46add_qml_test(Dash CardHeader)
46add_qml_test(Dash CardHeader)
47+add_qml_test(Dash CardTool)
47add_qml_test(Dash GenericScopeView IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
48add_qml_test(Dash GenericScopeView IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
48add_qml_test(Dash FilterGrids IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/plugins ${CMAKE_CURRENT_SOURCE_DIR}/plugins
49add_qml_test(Dash FilterGrids IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/plugins ${CMAKE_CURRENT_SOURCE_DIR}/plugins
49 ${CMAKE_BINARY_DIR}/tests/mocks)
50 ${CMAKE_BINARY_DIR}/tests/mocks)
tests/qmltests/Dash/CardHelpers.js+45
@@ -18,6 +18,51 @@
18
18
19var components = ["title", "art", "subtitle", "mascot", "emblem", "old-price", "price", "alt-price", "rating", "alt-rating", "summary"]
19var components = ["title", "art", "subtitle", "mascot", "emblem", "old-price", "price", "alt-price", "rating", "alt-rating", "summary"]
20
20
21+var defaultLayout = ' \
22+{ \
23+ "schema-version": 1, \
24+ "template": { \
25+ "category-layout": "grid", \
26+ "card-layout": "vertical", \
27+ "card-size": "medium", \
28+ "overlay-mode": null, \
29+ "collapsed-rows": 2 \
30+ }, \
31+ "components": { \
32+ "title": null, \
33+ "art": { \
34+ "aspect-ratio": 1.0, \
35+ "fill-mode": "crop" \
36+ }, \
37+ "subtitle": null, \
38+ "mascot": null, \
39+ "emblem": null, \
40+ "old-price": null, \
41+ "price": null, \
42+ "alt-price": null, \
43+ "rating": { \
44+ "type": "stars", \
45+ "range": [0, 5], \
46+ "full": "image://theme/rating-star-full", \
47+ "half": "image://theme/rating-star-half", \
48+ "empty": "image://theme/rating-star-empty" \
49+ }, \
50+ "alt-rating": null, \
51+ "summary": null \
52+ }, \
53+ "resources": {} \
54+}'
55+
56+var fullMapping = ' \
57+{ \
58+ "title": "title", \
59+ "art": "art", \
60+ "subtitle": "subtitle", \
61+ "mascot": "mascot", \
62+ "summary": "summary" \
63+}'
64+
65+
21function tryParse(json, errorLabel) {
66function tryParse(json, errorLabel) {
22 var o = undefined;
67 var o = undefined;
23 if (errorLabel !== undefined) {
68 if (errorLabel !== undefined) {
tests/qmltests/Dash/tst_Card.qml+50−69
@@ -27,41 +27,6 @@ Rectangle {
27 height: units.gu(72)
27 height: units.gu(72)
28 color: "#88FFFFFF"
28 color: "#88FFFFFF"
29
29
30− property string defaultLayout: '
31− {
32− "schema-version": 1,
33− "template": {
34− "category-layout": "grid",
35− "card-layout": "vertical",
36− "card-size": "medium",
37− "overlay-mode": null,
38− "collapsed-rows": 2
39− },
40− "components": {
41− "title": null,
42− "art": {
43− "aspect-ratio": 1.0,
44− "fill-mode": "crop"
45− },
46− "subtitle": null,
47− "mascot": null,
48− "emblem": null,
49− "old-price": null,
50− "price": null,
51− "alt-price": null,
52− "rating": {
53− "type": "stars",
54− "range": [0, 5],
55− "full": "image://theme/rating-star-full",
56− "half": "image://theme/rating-star-half",
57− "empty": "image://theme/rating-star-empty"
58− },
59− "alt-rating": null,
60− "summary": null
61− },
62− "resources": {}
63− }'
64−
65 property string cardData: '
30 property string cardData: '
66 {
31 {
67 "art": "../../tests/qmltests/Dash/artwork/music-player-design.png",
32 "art": "../../tests/qmltests/Dash/artwork/music-player-design.png",
@@ -71,58 +36,61 @@ Rectangle {
71 "summary": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
36 "summary": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
72 }'
37 }'
73
38
74− property string fullMapping: '
75− {
76− "title": "title",
77− "art": "art",
78− "subtitle": "subtitle",
79− "mascot": "mascot",
80− "summary": "summary"
81− }'
82−
83 property var cardsModel: [
39 property var cardsModel: [
84 {
40 {
85− "name": "Art, header, summary - vertical",
41+ "name": "Art, header, summary",
86− "layout": { "components": JSON.parse(fullMapping) }
42+ "layout": { "components": JSON.parse(Helpers.fullMapping) }
87 },
43 },
88 {
44 {
89− "name": "Art, header, summary - vertical, small",
45+ "name": "Art, header, summary - small",
90− "layout": { "template": { "card-size": "small" }, "components": JSON.parse(fullMapping) }
46+ "layout": { "template": { "card-size": "small" }, "components": JSON.parse(Helpers.fullMapping) }
91 },
47 },
92 {
48 {
93− "name": "Art, header, summary - vertical, large",
49+ "name": "Art, header, summary - large",
94− "layout": { "template": { "card-size": "large" }, "components": JSON.parse(fullMapping) }
50+ "layout": { "template": { "card-size": "large" }, "components": JSON.parse(Helpers.fullMapping) }
95 },
51 },
96 {
52 {
97− "name": "Art, header, summary - vertical, wide",
53+ "name": "Art, header, summary - wide",
98− "layout": { "components": Helpers.update(JSON.parse(root.fullMapping), { "art": { "aspect-ratio": 2 } }) }
54+ "layout": { "components": Helpers.update(JSON.parse(Helpers.fullMapping), { "art": { "aspect-ratio": 2 } }) }
99 },
55 },
100 {
56 {
101− "name": "Art, title - vertical, fitted",
57+ "name": "Art, title - fitted",
102− "layout": { "components": Helpers.update(JSON.parse(root.fullMapping), { "art": { "fill-mode": "fit" } }) }
58+ "layout": { "components": Helpers.update(JSON.parse(Helpers.fullMapping), { "art": { "fill-mode": "fit" } }) }
103 },
59 },
104 {
60 {
105 "name": "Art, header, summary - horizontal",
61 "name": "Art, header, summary - horizontal",
106 "layout": { "template": { "card-layout": "horizontal" },
62 "layout": { "template": { "card-layout": "horizontal" },
107− "components": JSON.parse(fullMapping) }
63+ "components": JSON.parse(Helpers.fullMapping) }
108 },
64 },
109 {
65 {
110− "name": "Art, header - vertical",
66+ "name": "Art, header",
111− "layout": { "components": Helpers.update(JSON.parse(root.fullMapping), { "summary": undefined }) }
67+ "layout": { "components": Helpers.update(JSON.parse(Helpers.fullMapping), { "summary": undefined }) }
112 },
68 },
113 {
69 {
70+ "name": "Art, summary",
71+ "layout": { "components": { "art": "art", "summary": "summary" } }
72+ },
73+ {
114− "name": "Header title only - horizontal",
74+ "name": "Header title only",
115− "layout": { "template": { "card-layout": "horizontal" },
116− "components": { "title": "title" } }
75+ "layout": { "components": { "title": "title" } }
117 },
76 },
118 ]
77 ]
119
78
79+ CardTool {
80+ id: cardTool
81+ template: card.template
82+ components: card.components
83+ }
84+
120 Card {
85 Card {
121 id: card
86 id: card
122 anchors { top: parent.top; left: parent.left; margins: units.gu(1) }
87 anchors { top: parent.top; left: parent.left; margins: units.gu(1) }
123
88
89+ width: cardTool.cardWidth || implicitWidth
90+ height: cardTool.cardHeight || implicitHeight
91+
124− template: Helpers.update(JSON.parse(root.defaultLayout), Helpers.tryParse(layoutArea.text, layoutError))['template'];
92+ template: Helpers.update(JSON.parse(Helpers.defaultLayout), Helpers.tryParse(layoutArea.text, layoutError))['template'];
125− components: Helpers.update(JSON.parse(root.defaultLayout), Helpers.tryParse(layoutArea.text, layoutError))['components'];
93+ components: Helpers.update(JSON.parse(Helpers.defaultLayout), Helpers.tryParse(layoutArea.text, layoutError))['components'];
126 cardData: Helpers.mapData(dataArea.text, components, dataError)
94 cardData: Helpers.mapData(dataArea.text, components, dataError)
127 }
95 }
128
96
@@ -245,8 +213,8 @@ Rectangle {
245 function test_card_size_data() {
213 function test_card_size_data() {
246 return [
214 return [
247 { tag: "Medium", width: units.gu(18.5), index: 0 },
215 { tag: "Medium", width: units.gu(18.5), index: 0 },
248− { tag: "Small", width: units.gu(12), size: "small", index: 0 },
216+ { tag: "Small", width: units.gu(12), index: 1 },
249− { tag: "Large", width: units.gu(38), size: "large", index: 0 },
217+ { tag: "Large", width: units.gu(38), index: 2 },
250 { tag: "Wide", width: units.gu(18.5), aspect: 0.5, index: 0 },
218 { tag: "Wide", width: units.gu(18.5), aspect: 0.5, index: 0 },
251 { tag: "Horizontal", width: units.gu(38), index: 5 },
219 { tag: "Horizontal", width: units.gu(38), index: 5 },
252 // Make sure card ends with header when there's no summary
220 // Make sure card ends with header when there's no summary
@@ -356,13 +324,26 @@ Rectangle {
356 tryCompareFunction(function() { return testCase.header.x === data.left() }, true);
324 tryCompareFunction(function() { return testCase.header.x === data.left() }, true);
357 }
325 }
358
326
327+ function test_summary_layout_data() {
328+ return [
329+ { tag: "With header", top: function() { return header.y + header.height }, index: 0 },
330+ { tag: "Without header", top: function() { return art.y + art.height }, index: 7 },
331+ ]
332+ }
333+
334+ function test_summary_layout(data) {
335+ selector.selectedIndex = data.index;
336+
337+ tryCompareFunction(function() { return testCase.summary.y === data.top() }, true);
338+ }
339+
359 function test_art_visibility() {
340 function test_art_visibility() {
360− selector.selectedIndex = 7
341+ selector.selectedIndex = 8;
361
342
362− tryCompare(testCase.artImage, "source", "")
343+ tryCompare(testCase.artImage, "source", "");
363− compare(testCase.art.visible, false)
344+ compare(testCase.art.visible, false);
364− compare(testCase.art.height, 0)
345+ compare(testCase.art.height, 0);
365− compare(testCase.art.width, 0)
346+ compare(testCase.art.width, 0);
366 }
347 }
367 }
348 }
368}
349}
tests/qmltests/Dash/tst_CardHeader.qml+3−3
@@ -120,9 +120,9 @@ Rectangle {
120
120
121 function test_dimensions_data() {
121 function test_dimensions_data() {
122 return [
122 return [
123− { tag: "Column width", object: column, width: cardHeader.width - testCase.outerRow.spacing * 2 },
123+ { tag: "Column width", object: column, width: cardHeader.width },
124− { tag: "Column width", object: column, width: cardHeader.width - mascot.width - testCase.outerRow.spacing * 3, mascot: "artwork/avatar.png" },
124+ { tag: "Column width with mascot", object: column, width: cardHeader.width - mascot.width - outerRow.margins * 3, mascot: "artwork/avatar.png" },
125− { tag: "Header height", object: cardHeader, height: function() { return subtitleLabel.y + subtitleLabel.height + outerRow.spacing * 2 } },
125+ { tag: "Header height", object: cardHeader, height: function() { return subtitleLabel.y + subtitleLabel.height + outerRow.margins * 2 } },
126 ]
126 ]
127 }
127 }
128
128
tests/qmltests/Dash/tst_CardTool.qmlnew file+300
@@ -0,0 +1,300 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtTest 1.0
19+import Ubuntu.Components 0.1
20+import Unity.Test 0.1 as UT
21+import "../../../qml/Dash"
22+import "CardHelpers.js" as Helpers
23+
24+Rectangle {
25+ id: root
26+ width: units.gu(80)
27+ height: units.gu(72)
28+ color: "#88FFFFFF"
29+
30+ property var cardsModel: [
31+ {
32+ "name": "Art, header, summary",
33+ "layout": { "components": JSON.parse(Helpers.fullMapping) }
34+ },
35+ {
36+ "name": "Header, summary",
37+ "layout": { "components": Helpers.update(JSON.parse(Helpers.fullMapping), { "art": undefined }) }
38+ },
39+ {
40+ "name": "Art, header",
41+ "layout": { "components": Helpers.update(JSON.parse(Helpers.fullMapping), { "summary": undefined }) }
42+ },
43+ {
44+ "name": "Header only",
45+ "layout": { "components": Helpers.update(JSON.parse(Helpers.fullMapping), { "art": undefined, "summary": undefined }) }
46+ },
47+ {
48+ "name": "Header title only",
49+ "layout": { "components": { "title": "title" } }
50+ },
51+ {
52+ "name": "Header title and subtitle",
53+ "layout": { "components": { "title": "title", "subtitle": "subtitle" } }
54+ },
55+ {
56+ "name": "Header title and mascot",
57+ "layout": { "components": { "title": "title", "mascot": "mascot" } }
58+ },
59+ {
60+ "name": "Art, header, summary - small",
61+ "layout": { "template": { "card-size": "small" }, "components": JSON.parse(Helpers.fullMapping) }
62+ },
63+ {
64+ "name": "Art, header, summary - large",
65+ "layout": { "template": { "card-size": "large" }, "components": JSON.parse(Helpers.fullMapping) }
66+ },
67+ {
68+ "name": "Art, header, summary - horizontal",
69+ "layout": { "template": { "card-layout": "horizontal" }, "components": JSON.parse(Helpers.fullMapping) }
70+ },
71+ {
72+ "name": "Art, header - portrait",
73+ "layout": { "components": Helpers.update(JSON.parse(Helpers.fullMapping), { "art": {"aspect-ratio": 0.5, "summary": undefined }}) }
74+ },
75+ ]
76+
77+ CardTool {
78+ id: cardTool
79+
80+ template: Helpers.update(JSON.parse(Helpers.defaultLayout), Helpers.tryParse(layoutArea.text, layoutError))['template'];
81+ components: Helpers.update(JSON.parse(Helpers.defaultLayout), Helpers.tryParse(layoutArea.text, layoutError))['components'];
82+ viewWidth: units.gu(Math.round(widthSlider.value))
83+ }
84+
85+ Column {
86+ width: units.gu(38)
87+ anchors { left: parent.left; top: parent.top; margins: units.gu(1) }
88+
89+ Repeater {
90+ model: [
91+ { label: "View width", value: cardTool.viewWidth && cardTool.viewWidth / units.gu(1) },
92+ { label: "Card width", value: cardTool.cardWidth && cardTool.cardWidth / units.gu(1) },
93+ { label: "Card height", value: cardTool.cardHeight && cardTool.cardHeight / units.gu(1) },
94+ ]
95+
96+ delegate: Row {
97+ anchors { left: parent.left; right: parent.right; margins: units.gu(3) }
98+
99+ Label {
100+ height: units.gu(5)
101+ width: units.gu(25)
102+ text: modelData.label
103+ verticalAlignment: Text.AlignVCenter
104+ }
105+
106+ Label {
107+ height: units.gu(5)
108+ text: modelData.value || "undefined"
109+ verticalAlignment: Text.AlignVCenter
110+ }
111+ }
112+ }
113+
114+ Card {
115+ id: card
116+ template: cardTool.template;
117+ components: cardTool.components;
118+ cardData: Helpers.mapData(dataArea.text, components, dataError)
119+
120+ width: cardTool.cardWidth || implicitWidth
121+ height: cardTool.cardHeight || implicitHeight
122+
123+ clip: true
124+
125+ Rectangle {
126+ anchors.fill: parent
127+ color: "transparent"
128+ border.width: 1
129+ border.color: "green"
130+ }
131+ }
132+ }
133+
134+ Rectangle {
135+ anchors { top: parent.top; bottom: parent.bottom; right: parent.right}
136+ width: units.gu(40)
137+ color: "lightgrey"
138+
139+ Column {
140+ anchors { fill: parent; margins: units.gu(1) }
141+ spacing: units.gu(1)
142+
143+ OptionSelector {
144+ id: selector
145+ model: cardsModel
146+ delegate: OptionSelectorDelegate { text: modelData.name }
147+ onSelectedIndexChanged: updateAreas()
148+ Component.onCompleted: updateAreas()
149+
150+ function updateAreas() {
151+ var element = cardsModel[selectedIndex];
152+ if (element) {
153+ layoutArea.text = JSON.stringify(element.layout, undefined, 2) || "{}";
154+ } else {
155+ layoutArea.text = "";
156+ }
157+ }
158+ }
159+
160+ OptionSelector {
161+ id: layoutSelector
162+ model: [
163+ { label: "Grid", layout: "grid" },
164+ { label: "Organic Grid", layout: "organic-grid" },
165+ { label: "Horizontal Journal", layout: "journal" },
166+ { label: "Vertical Journal", layout: "vertical-journal" },
167+ { label: "Carousel", layout: "carousel" },
168+ ]
169+ delegate: OptionSelectorDelegate { text: modelData.label }
170+ onSelectedIndexChanged: {
171+ var element = model[selectedIndex];
172+ if (element && cardTool.template["category-layout"] !== element.layout) {
173+ var current = JSON.parse(layoutArea.text);
174+ var update = { "template": { "category-layout": element.layout }}
175+ layoutArea.text = JSON.stringify(Helpers.update(current, update), undefined, 2) || "{}";
176+ }
177+ }
178+ }
179+
180+ Label {
181+ anchors { left: parent.left; right: parent.right }
182+ height: units.gu(3)
183+ verticalAlignment: Text.AlignBottom
184+ text: "View width:"
185+ }
186+
187+ Slider {
188+ id: widthSlider
189+ minimumValue: 30
190+ maximumValue: 140
191+ live: true
192+ value: 40
193+ }
194+
195+ TextArea {
196+ id: layoutArea
197+ anchors { left: parent.left; right: parent.right }
198+ height: units.gu(25)
199+ }
200+
201+ Label {
202+ id: layoutError
203+ anchors { left: parent.left; right: parent.right }
204+ height: units.gu(4)
205+ color: "orange"
206+ }
207+
208+ TextArea {
209+ id: dataArea
210+ anchors { left: parent.left; right: parent.right }
211+ height: units.gu(25)
212+ text: JSON.stringify(cardTool.priv.cardData, undefined, 2)
213+ }
214+
215+ Label {
216+ id: dataError
217+ anchors { left: parent.left; right: parent.right }
218+ height: units.gu(4)
219+ color: "orange"
220+ }
221+ }
222+
223+ Connections {
224+ target: cardTool
225+ onTemplateChanged: {
226+ switch(cardTool.template["category-layout"]) {
227+ case "organic-grid": layoutSelector.selectedIndex = 1; break;
228+ case "journal": layoutSelector.selectedIndex = 2; break;
229+ case "vertical-journal": layoutSelector.selectedIndex = 3; break;
230+ case "carousel": layoutSelector.selectedIndex = 4; break;
231+ case "grid":
232+ default: layoutSelector.selectedIndex = 0; break;
233+ }
234+ }
235+ }
236+ }
237+
238+ UT.UnityTestCase {
239+ id: testCase
240+ name: "Card"
241+
242+ when: windowShown
243+
244+ function cleanup() {
245+ selector.selectedIndex = -1;
246+ layoutSelector.selectedIndex = -1;
247+ }
248+
249+ function test_card_size_data() {
250+ return [
251+ { tag: "Medium", width: units.gu(18.5), height: function() { return card.height }, index: 0 },
252+ { tag: "No art", width: units.gu(18.5), height: function() { return card.height }, index: 1 },
253+ { tag: "No summary", width: units.gu(18.5), height: function() { return card.height }, index: 2 },
254+ { tag: "Just header", width: units.gu(18.5), height: function() { return card.height }, index: 3 },
255+ { tag: "Just title", width: units.gu(18.5), height: function() { return card.height }, index: 4 },
256+ { tag: "Title and subtitle", width: units.gu(18.5), height: function() { return card.height }, index: 5 },
257+ { tag: "Title and mascot", width: units.gu(18.5), height: function() { return card.height }, index: 6 },
258+ { tag: "Small", width: units.gu(12), height: function() { return card.height }, index: 7 },
259+ { tag: "Large", width: units.gu(38), height: function() { return card.height }, index: 8 },
260+ { tag: "Horizontal", width: units.gu(38), height: function() { return card.height }, index: 9 },
261+ { tag: "OrganicGrid", width: undefined, height: undefined, index: 0, layout_index: 1},
262+ { tag: "Journal", width: undefined, height: units.gu(20), size: 20, index: 0, layout_index: 2 },
263+ { tag: "OversizedJournal", width: undefined, height: units.gu(18.5), size: 40, index: 0, layout_index: 2 },
264+ { tag: "VerticalJournal", width: units.gu(18.5), height: undefined, index: 0, layout_index: 3 },
265+ { tag: "SmallCarousel", width: units.gu(18), height: units.gu(18), viewWidth: 30, index: 0, layout_index: 4},
266+ { tag: "MediumCarousel", width: units.gu(22), height: units.gu(22), viewWidth: 84, index: 0, layout_index: 4},
267+ { tag: "LargeCarousel", width: units.gu(26), height: units.gu(26), viewWidth: 140, index: 0, layout_index: 4},
268+ { tag: "PortraitCarousel", width: units.gu(22), height: units.gu(44), viewWidth: 84, index: 10, layout_index: 4},
269+ ]
270+ }
271+
272+ function test_card_size(data) {
273+ selector.selectedIndex = data.index;
274+ if (data.hasOwnProperty("layout_index")) {
275+ layoutSelector.selectedIndex = data.layout_index;
276+ }
277+
278+ if (data.hasOwnProperty("viewWidth")) {
279+ widthSlider.value = data.viewWidth;
280+ }
281+
282+ if (data.hasOwnProperty("size")) {
283+ cardTool.template['card-size'] = data.size;
284+ cardTool.templateChanged();
285+ }
286+
287+ if (data.hasOwnProperty("width")) {
288+ if (typeof data.width === "function") {
289+ tryCompareFunction(function() { return cardTool.cardWidth === data.width() }, true);
290+ } else tryCompare(cardTool, "cardWidth", data.width);
291+ }
292+
293+ if (data.hasOwnProperty("height")) {
294+ if (typeof data.height === "function") {
295+ tryCompareFunction(function() { return cardTool.cardHeight === data.height() }, true);
296+ } else tryCompare(cardTool, "cardHeight", data.height);
297+ }
298+ }
299+ }
300+}