on xenial help sign in
Support fallback images for dash cards and preview widgets
14ad122Albert Astals Cid
plugins/Dash/CardCreator.js+8
@@ -362,6 +362,10 @@ function cardString(template, components) {
362 }
362 }
363
363
364 code += kArtShapeHolderCode.arg(artAnchors).arg(widthCode).arg(heightCode);
364 code += kArtShapeHolderCode.arg(artAnchors).arg(widthCode).arg(heightCode);
365+ var fallback = components["art"] && components["art"]["fallback"] || "";
366+ if (fallback !== "") {
367+ code += 'Connections { target: artShapeLoader.item ? artShapeLoader.item.image : null; onStatusChanged: if (artShapeLoader.item.image.status === Image.Error) artShapeLoader.item.image.source = "%1"; } \n'.arg(fallback);
368+ }
365 } else {
369 } else {
366 code += 'readonly property size artShapeSize: Qt.size(-1, -1);\n'
370 code += 'readonly property size artShapeSize: Qt.size(-1, -1);\n'
367 }
371 }
@@ -441,6 +445,10 @@ function cardString(template, components) {
441
445
442 var mascotImageVisible = useMascotShape ? 'false' : 'showHeader';
446 var mascotImageVisible = useMascotShape ? 'false' : 'showHeader';
443 mascotCode = kMascotImageCode.arg(mascotAnchors).arg(mascotImageVisible);
447 mascotCode = kMascotImageCode.arg(mascotAnchors).arg(mascotImageVisible);
448+ var fallback = components["mascot"] && components["mascot"]["fallback"] || "";
449+ if (fallback !== "") {
450+ code += 'Connections { target: mascotImage.image; onStatusChanged: if (mascotImage.image.status === Image.Error) mascotImage.source = "%1"; } \n'.arg(fallback);
451+ }
444 }
452 }
445
453
446 var summaryColorWithBackground = 'backgroundLoader.active && backgroundLoader.item && root.scopeStyle ? root.scopeStyle.getTextColor(backgroundLoader.item.luminance) : (backgroundLoader.item && backgroundLoader.item.luminance > 0.7 ? Theme.palette.normal.baseText : "white")';
454 var summaryColorWithBackground = 'backgroundLoader.active && backgroundLoader.item && root.scopeStyle ? root.scopeStyle.getTextColor(backgroundLoader.item.luminance) : (backgroundLoader.item && backgroundLoader.item.luminance > 0.7 ? Theme.palette.normal.baseText : "white")';
qml/Components/ZoomableImage.qml+1
@@ -30,6 +30,7 @@ Item {
30 property var zoomable: false
30 property var zoomable: false
31 property alias imageStatus: imageRenderer.status
31 property alias imageStatus: imageRenderer.status
32 property alias asynchronous: imageRenderer.asynchronous
32 property alias asynchronous: imageRenderer.asynchronous
33+ readonly property alias status: imageRenderer.status
33
34
34 Flickable {
35 Flickable {
35 id: flickable
36 id: flickable
qml/Dash/Previews/PreviewHeader.qml+6
@@ -22,6 +22,7 @@ import "../"
22/*! This preview widget shows a header
22/*! This preview widget shows a header
23 * The title comes in widgetData["title"]
23 * The title comes in widgetData["title"]
24 * The mascot comes in widgetData["mascot"]
24 * The mascot comes in widgetData["mascot"]
25+ * The mascot fall back image comes in widgetData["fallback"]
25 * The subtitle comes in widgetData["subtitle"]
26 * The subtitle comes in widgetData["subtitle"]
26 * The attributes comes in widgetData["attributes"]
27 * The attributes comes in widgetData["attributes"]
27 */
28 */
@@ -35,11 +36,15 @@ PreviewWidget {
35 id: headerRoot
36 id: headerRoot
36 objectName: "innerPreviewHeader"
37 objectName: "innerPreviewHeader"
37 readonly property url mascot: root.widgetData["mascot"] || ""
38 readonly property url mascot: root.widgetData["mascot"] || ""
39+ readonly property url fallback: root.widgetData["fallback"] || ""
38 readonly property string title: root.widgetData["title"] || ""
40 readonly property string title: root.widgetData["title"] || ""
39 readonly property string subtitle: root.widgetData["subtitle"] || ""
41 readonly property string subtitle: root.widgetData["subtitle"] || ""
40 readonly property var attributes: root.widgetData["attributes"] || null
42 readonly property var attributes: root.widgetData["attributes"] || null
41 readonly property color fontColor: root.scopeStyle ? root.scopeStyle.foreground : Theme.palette.normal.baseText
43 readonly property color fontColor: root.scopeStyle ? root.scopeStyle.foreground : Theme.palette.normal.baseText
42
44
45+ // Rewire the source since we may have unwired it on onStatusChanged
46+ onMascotChanged: if (mascotShapeLoader.item) mascotShapeLoader.item.image.source = mascot;
47+
43 implicitHeight: row.height + row.margins * 2
48 implicitHeight: row.height + row.margins * 2
44 width: parent.width
49 width: parent.width
45
50
@@ -82,6 +87,7 @@ PreviewWidget {
82 fillMode: Image.PreserveAspectCrop
87 fillMode: Image.PreserveAspectCrop
83 horizontalAlignment: Image.AlignHCenter
88 horizontalAlignment: Image.AlignHCenter
84 verticalAlignment: Image.AlignVCenter
89 verticalAlignment: Image.AlignVCenter
90+ onStatusChanged: if (status === Image.Error) source = headerRoot.fallback;
85 }
91 }
86 }
92 }
87 }
93 }
qml/Dash/Previews/PreviewImageGallery.qml+14
@@ -20,6 +20,7 @@ import "../../Components"
20
20
21/*! This preview widget shows a horizontal list of images.
21/*! This preview widget shows a horizontal list of images.
22 * The URIs for the images should be an array in widgetData["sources"].
22 * The URIs for the images should be an array in widgetData["sources"].
23+ * Images fall back to widgetData["fallback"] if loading fails
23 */
24 */
24
25
25PreviewWidget {
26PreviewWidget {
@@ -72,6 +73,14 @@ PreviewWidget {
72 overlay.show();
73 overlay.show();
73 }
74 }
74 }
75 }
76+
77+ Connections {
78+ target: sourceImage
79+ // If modelData would change after failing to load it would not be
80+ // reloaded since the source binding is destroyed by the source = fallback
81+ // But at the moment the model never changes
82+ onStatusChanged: if (sourceImage.status === Image.Error) sourceImage.source = widgetData["fallback"];
83+ }
75 }
84 }
76 }
85 }
77
86
@@ -122,6 +131,11 @@ PreviewWidget {
122 source: modelData ? modelData : ""
131 source: modelData ? modelData : ""
123 fillMode: Image.PreserveAspectFit
132 fillMode: Image.PreserveAspectFit
124 sourceSize { width: screenshot.width; height: screenshot.height }
133 sourceSize { width: screenshot.width; height: screenshot.height }
134+
135+ // If modelData would change after failing to load it would not be
136+ // reloaded since the source binding is destroyed by the source = fallback
137+ // But at the moment the model never changes
138+ onStatusChanged: if (status === Image.Error) source = widgetData["fallback"];
125 }
139 }
126
140
127 MouseArea {
141 MouseArea {
qml/Dash/Previews/PreviewZoomableImage.qml+13
@@ -21,6 +21,7 @@ import "../../Components"
21/*! \brief Preview widget for image.
21/*! \brief Preview widget for image.
22
22
23 This widget shows image contained in widgetData["source"],
23 This widget shows image contained in widgetData["source"],
24+ and falls back to widgetData["fallback"] if loading fails
24 can be zoomable accordingly with widgetData["zoomable"].
25 can be zoomable accordingly with widgetData["zoomable"].
25 */
26 */
26
27
@@ -53,6 +54,14 @@ PreviewWidget {
53 overlay.show();
54 overlay.show();
54 }
55 }
55 }
56 }
57+
58+ Connections {
59+ target: lazyImage.sourceImage
60+ // If modelData would change after failing to load it would not be
61+ // reloaded since the source binding is destroyed by the source = fallback
62+ // But at the moment the model never changes
63+ onStatusChanged: if (lazyImage.sourceImage.status === Image.Error) lazyImage.sourceImage.source = widgetData["fallback"];
64+ }
56 }
65 }
57
66
58 PreviewOverlay {
67 PreviewOverlay {
@@ -67,6 +76,10 @@ PreviewWidget {
67 anchors.fill: parent
76 anchors.fill: parent
68 source: widgetData["source"]
77 source: widgetData["source"]
69 zoomable: widgetData["zoomable"] ? widgetData["zoomable"] : false
78 zoomable: widgetData["zoomable"] ? widgetData["zoomable"] : false
79+ // If modelData would change after failing to load it would not be
80+ // reloaded since the source binding is destroyed by the source = fallback
81+ // But at the moment the model never changes
82+ onStatusChanged: if (status === Image.Error) source = widgetData["fallback"];
70 }
83 }
71 }
84 }
72}
85}
tests/qmltests/Dash/Previews/tst_PreviewHeader.qml+30
@@ -38,6 +38,22 @@ Rectangle {
38 "attributes": [{"value":"text1","icon":"image://theme/ok"},{"value":"text2","icon":"image://theme/cancel"}]
38 "attributes": [{"value":"text1","icon":"image://theme/ok"},{"value":"text2","icon":"image://theme/cancel"}]
39 }
39 }
40
40
41+ property var brokenheaderjson: {
42+ "title": "THE TITLE",
43+ "subtitle": "Something catchy",
44+ "mascot": "bad_path",
45+ "fallback": "",
46+ "attributes": [{"value":"text1","icon":"image://theme/ok"},{"value":"text2","icon":"image://theme/cancel"}]
47+ }
48+
49+ property var fallbackheaderjson: {
50+ "title": "THE TITLE",
51+ "subtitle": "Something catchy",
52+ "mascot": "bad_path2",
53+ "fallback": "../graphics/play_button.png",
54+ "attributes": [{"value":"text1","icon":"image://theme/ok"},{"value":"text2","icon":"image://theme/cancel"}]
55+ }
56+
41 PreviewHeader {
57 PreviewHeader {
42 id: previewHeader
58 id: previewHeader
43 widgetData: headerjson
59 widgetData: headerjson
@@ -121,5 +137,19 @@ Rectangle {
121 compare(innerPreviewHeader.subtitle, "Something catchy");
137 compare(innerPreviewHeader.subtitle, "Something catchy");
122 compare(innerPreviewHeader.mascot.toString().slice(-24), "graphics/play_button.png");
138 compare(innerPreviewHeader.mascot.toString().slice(-24), "graphics/play_button.png");
123 }
139 }
140+
141+ function test_fallback() {
142+ previewHeader.widgetData = brokenheaderjson;
143+ tryCompareFunction(function() { return findChild(previewHeader, "mascotShape") != null }, true);
144+ var mascot = findChild(previewHeader, "mascotShape");
145+ compare(mascot.visible, false);
146+
147+ previewHeader.widgetData = {};
148+ previewHeader.widgetData = fallbackheaderjson;
149+ tryCompareFunction(function() { return findChild(previewHeader, "mascotShape") != null }, true);
150+ var mascot = findChild(previewHeader, "mascotShape");
151+ tryCompare(mascot, "visible", true);
152+ tryCompare(mascot.image, "status", Image.Ready);
153+ }
124 }
154 }
125}
155}
tests/qmltests/Dash/Previews/tst_PreviewImageGallery.qml+21−1
@@ -33,10 +33,21 @@ Rectangle {
33 "sources": [
33 "sources": [
34 "../../graphics/phone_background.jpg",
34 "../../graphics/phone_background.jpg",
35 "../../graphics/tablet_background.jpg",
35 "../../graphics/tablet_background.jpg",
36− "../../graphics/clock@18.png"
36+ "../../graphics/clock@18.png",
37+ "../../graphics/borked"
37 ]
38 ]
38 }
39 }
39
40
41+ property var sourcesModel1WithFallback: {
42+ "sources": [
43+ "../../graphics/phone_background.jpg",
44+ "../../graphics/tablet_background.jpg",
45+ "../../graphics/clock@18.png",
46+ "../../graphics/borked"
47+ ]
48+ , "fallback": "../../graphics/clock@18.png"
49+ }
50+
40 PreviewImageGallery {
51 PreviewImageGallery {
41 id: imageGallery
52 id: imageGallery
42 width: parent.width
53 width: parent.width
@@ -103,5 +114,14 @@ Rectangle {
103 tryCompare(overlayListView, "currentIndex", data.index);
114 tryCompare(overlayListView, "currentIndex", data.index);
104 verify(image.source === overlayListView.currentItem.source);
115 verify(image.source === overlayListView.currentItem.source);
105 }
116 }
117+
118+ function test_fallback() {
119+ var image3 = findChild(imageGallery, "previewImage3");
120+ tryCompare(image3, "state", "error");
121+ imageGallery.widgetData = sourcesModel0;
122+ imageGallery.widgetData = sourcesModel1WithFallback;
123+ image3 = findChild(imageGallery, "previewImage3");
124+ tryCompare(image3, "state", "ready");
125+ }
106 }
126 }
107}
127}
tests/qmltests/Dash/Previews/tst_PreviewZoomableImage.qml+27−6
@@ -26,7 +26,7 @@ Rectangle {
26 color: "lightgrey"
26 color: "lightgrey"
27
27
28 property var widgetData0: {
28 property var widgetData0: {
29− "source": Qt.resolvedUrl("../artwork/checkers.png"),
29+ "source": "../../graphics/phone_background.jpg",
30 "zoomable": false
30 "zoomable": false
31 }
31 }
32
32
@@ -34,21 +34,35 @@ Rectangle {
34 "source": ""
34 "source": ""
35 }
35 }
36
36
37− PreviewZoomableImage {
37+ property var widgetData2: {
38− id: zoomableImage
38+ "source": "fadsasf",
39+ "fallback": "../../graphics/phone_background.jpg"
40+ }
41+
42+ Loader {
43+ id: loader
39 anchors.centerIn: parent
44 anchors.centerIn: parent
45+ sourceComponent: PreviewZoomableImage {
40− widgetData: widgetData0
46+ widgetData: widgetData0
47+ }
41 }
48 }
42
49
50+ property alias zoomableImage: loader.item
51+
43 UT.UnityTestCase {
52 UT.UnityTestCase {
44 name: "PreviewZoomableImageTest"
53 name: "PreviewZoomableImageTest"
45 when: windowShown
54 when: windowShown
46
55
47− property Item lazyImage: findChild(zoomableImage, "lazyImage");
56+ property Item lazyImage
48− property Item overlay: findChild(zoomableImage.rootItem, "overlay");
57+ property Item overlay
49
58
50 function init() {
59 function init() {
60+ // Use a loader so we start from scratch each time
61+ loader.active = false;
62+ loader.active = true;
63+ lazyImage = findChild(zoomableImage, "lazyImage");
51 waitForRendering(zoomableImage);
64 waitForRendering(zoomableImage);
65+ overlay = findChild(zoomableImage.rootItem, "overlay");
52 waitForRendering(overlay);
66 waitForRendering(overlay);
53 }
67 }
54
68
@@ -79,5 +93,12 @@ Rectangle {
79 mouseClick(overlayCloseButton);
93 mouseClick(overlayCloseButton);
80 tryCompare(overlay, "visible", false);
94 tryCompare(overlay, "visible", false);
81 }
95 }
96+
97+ function test_fallback() {
98+ zoomableImage.widgetData = widgetData2;
99+ waitForRendering(zoomableImage);
100+ waitForRendering(lazyImage);
101+ tryCompare(lazyImage, "state", "ready");
102+ }
82 }
103 }
83}
104}
tests/qmltests/Dash/tst_Card.qml+30
@@ -224,6 +224,7 @@ Rectangle {
224 property Item mascotImage: findChild(card, "mascotImage");
224 property Item mascotImage: findChild(card, "mascotImage");
225
225
226 function init() {
226 function init() {
227+ cardTool.components = Qt.binding(function() { return Helpers.update(JSON.parse(Helpers.defaultLayout), Helpers.tryParse(layoutArea.text, layoutError))['components']; });
227 loader.visible = true;
228 loader.visible = true;
228 }
229 }
229
230
@@ -424,6 +425,7 @@ Rectangle {
424 ];
425 ];
425 }
426 }
426
427
428+
427 function test_background(data) {
429 function test_background(data) {
428 selector.selectedIndex = data.index;
430 selector.selectedIndex = data.index;
429 waitForRendering(selector);
431 waitForRendering(selector);
@@ -454,6 +456,34 @@ Rectangle {
454 }
456 }
455 }
457 }
456
458
459+ function test_fallback() {
460+ selector.selectedIndex = 0;
461+ waitForRendering(card);
462+
463+ card.cardData["art"] = "somethingbroken";
464+ card.cardDataChanged();
465+ waitForRendering(card);
466+ tryCompare(art, "visible", false);
467+
468+ cardTool.components["art"]["fallback"] = Qt.resolvedUrl("artwork/emblem.png");
469+ cardTool.componentsChanged();
470+ card.cardData["art"] = "somethingbroken";
471+ card.cardDataChanged();
472+ waitForRendering(card);
473+ tryCompare(art, "visible", true);
474+
475+ card.cardData["mascot"] = "somethingbroken2";
476+ card.cardDataChanged();
477+ compare(mascotImage.image.status, Image.Error);
478+
479+ cardTool.components["mascot"] = {"fallback": Qt.resolvedUrl("artwork/emblem.png")};
480+ cardTool.componentsChanged();
481+ card.cardData["mascot"] = "somethingbroken2";
482+ card.cardDataChanged();
483+ waitForRendering(card);
484+ tryCompare(mascotImage.image, "status", Image.Ready);
485+ }
486+
457 function test_font_weights_data() {
487 function test_font_weights_data() {
458 return [
488 return [
459 { tag: "Title only", index: 8, weight: Font.Normal },
489 { tag: "Title only", index: 8, weight: Font.Normal },