Skip to content

Instantly share code, notes, and snippets.

@UniversalSuperBox
Last active June 3, 2021 21:35
Show Gist options
  • Save UniversalSuperBox/e2d287bc1a65a82dba2decde5fe56a95 to your computer and use it in GitHub Desktop.
Save UniversalSuperBox/e2d287bc1a65a82dba2decde5fe56a95 to your computer and use it in GitHub Desktop.
Pure-QML solution to select the first loadable image in a list.

This is a rewrite of Lomiri's WallpaperResolver that takes its image candidates and loads each sequentially.

At its base level, what I've created is a Pure-QML solution that takes a long list of potentially valid images as candidates, then loads each one in sequence. Once a valid image is found, it is returned as background. If no valid image is found, the last item in candidates is returned.

I built it to prevent redundant image loading inside Lomiri. The current WallpaperResolver has the same goal as this: return the first valid image in a list. It achieves its goal by taking each image that is passed in and loading it as a member of a Repeater. This is great, but it means that every potential image is loaded, not just the first valid image. If three valid images were passed in, the first was returned but the other two loaded anyway! What's worse, the entire Shell's creation hung on this process since the binding required that the entire Repeater was available and was assigned at startup. What a waste.

This solution is good, I think. It only loads the first image. However, it's pretty complex. Due to optimizations inside Qt, we need to avoid setting the Image's source to the same value twice, and we need to avoid setting it to an invalid value at all costs. Otherwise, the Image's source doesn't actually change and resolution never completes.

However, I'm not sure if this complex of a solution is required when we only have two images to load at any time. While we pass in three candidates, we don't need to load the last one since we assume that it's always loadable. That's a much easier change to make.

We can also set asynchronous: true on the remaining images to cause them to load in parallel. That will cause the slowest-loading image, rather than the total time of each image, to be the primary slowdown. That requires a change to make the image loading look a little smoother (right now it just flashes on screen), but it's perfectly doable.

So it's here as a gist rather than a PR.

/*
* Copyright 2021 UBports Foundation
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
* to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
import QtQuick 2.4
Item {
id: root
objectName: "wallpaperResolver"
// Provide a list of potential image URLs to resolve here, preferred ones
// first.
// If no images can be loaded, the final candidate will be returned whether
// or not it can load.
property var candidates: []
property url background: internal.resolver ? internal.resolver.selected : ""
// If the Image ends up with an unset or empty source, it won't transition
// to the next source in line. All of the candidates we pass to it must be
// non-empty URLs.
readonly property var filteredCandidates: {
var filtered = []
for (var i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
if (
(typeof candidate === "object" || typeof candidate === "string") // Filter out most things that aren't URLs
&& candidate !== ""
&& candidate !== filtered.slice(-1)[0] // Don't test the same image multiple times
&& candidate !== null
&& candidate !== undefined
) {
filtered.push(candidate);
}
}
return filtered
}
QtObject {
id: internal
property var resolver: null
}
function reloadResolver() {
destroyResolver();
internal.resolver = resolverComponent.createObject(root, {candidates: root.filteredCandidates});
}
function destroyResolver() {
if (internal.resolver !== null) {
internal.resolver.destroy();
internal.resolver = null;
}
}
onFilteredCandidatesChanged: {
if (filteredCandidates.length > 0) {
reloadResolver();
} else {
destroyResolver();
}
}
Component {
id: resolverComponent
Item {
id: resolverItem
property var candidates
property url selected: {
if (image.status === Image.Ready) {
return image.source;
}
if (
image.currentCandidateIndex === candidates.length - 2
&& (image.status === Image.Error || image.status === Image.Null)
) {
return candidates.slice(-1)[0];
}
return "";
}
Image {
id: image
asynchronous: true
visible: false
property int currentCandidateIndex: 0
sourceSize: Qt.size(1, 1)
source: resolverItem.candidates[currentCandidateIndex]
onStatusChanged: {
if (status === Image.Error || status === Image.Null) {
var finalCandidateIndex = resolverItem.candidates.length - 1;
var nextCandidateIndex = currentCandidateIndex + 1;
if (nextCandidateIndex >= finalCandidateIndex) {
return;
}
currentCandidateIndex = nextCandidateIndex;
}
}
}
}
}
}
/*
* Copyright (C) 2015 Canonical, Ltd.
* Copyright (C) 2021 UBports Foundation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.4
import QtTest 1.0
// import Ubuntu.Components 1.3
// import Unity.Test 0.1
import "../../../qml/Components"
Image {
id: root
width: 70
height: 70
source: wallpaperResolver.background
readonly property url blue: Qt.resolvedUrl("../../data/unity/backgrounds/blue.png")
readonly property url red: Qt.resolvedUrl("../../data/unity/backgrounds/red.png")
WallpaperResolver {
id: wallpaperResolver
}
TestCase {
id: testCase
name: "WallpaperResolver"
when: windowShown
function test_background_data() {
return [
{tag: "empty-candidates",
list: [],
output: ""},
{tag: "blank-candidate",
list: [""],
output: ""},
{tag: "blank-urls",
list: [Qt.resolvedUrl(""), Qt.resolvedUrl(""), root.blue],
output: root.blue},
{tag: "invalid-urls",
list: [Qt.resolvedUrl("/first"), Qt.resolvedUrl("/middle"), root.blue],
output: root.blue},
{tag: "valid-after-blanks",
list: ["", "", root.red],
output: root.red},
// Ensure that the WallpaperResolver doesn't get stuck if it
// sees the same invalid wallpaper multiple times in a row
{tag: "valid-after-the-same-invalid",
list: ["/first", "/first", "/first", root.red],
output: root.red},
// Being able to filter out bad values is not necessarily
// required since we trust WallpaperResolver's users, but I was
// already writing the filter statement
{tag: "naughty",
list: [null, undefined, "", NaN, 1.0, 1, root.red, null],
output: root.red},
{tag: "none-valid",
list: ["/first", "/middle", "/last"],
output: Qt.resolvedUrl("/last")},
{tag: "first-valid",
list: [root.blue, "/middle", "/last"],
output: root.blue},
{tag: "middle-valid",
list: ["/first", root.red, "/last"],
output: root.red},
{tag: "last-valid",
list: ["/first", "/middle", root.red],
output: root.red},
{tag: "multiple-valid",
list: [root.blue, root.red],
output: root.blue},
{tag: "multiple-valid-after-multiple-invalid",
list: ["/first", "/middle", "/last", root.blue, root.red],
output: root.blue},
]
}
function init() {
// Make sure we don't have our next test compare() to the results
// of the last test
wallpaperResolver.candidates = [root.blue];
tryCompare(wallpaperResolver, "background", root.blue);
wallpaperResolver.candidates = [];
tryCompare(wallpaperResolver, "background", "")
}
function test_background(data) {
wallpaperResolver.candidates = data.list;
tryCompare(wallpaperResolver, "background", data.output);
}
function test_reload_with_blanks() {
wallpaperResolver.candidates = ["", "", root.red];
tryCompare(wallpaperResolver, "background", root.red);
wallpaperResolver.candidates = ["", "", root.blue];
tryCompare(wallpaperResolver, "background", root.blue);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment