Keep image picker work bounded for large theme collections
This commit is contained in:
1 parent
a85e29abb5
commit
2ba1015ef2
6 files changed
+240
-33
No files matched your search
@@ -25,6 +25,7 @@ Item {
|
||||
property bool showLabels: false
|
||||
property bool filterable: false
|
||||
property bool layoutSettled: false
|
||||
property bool neighborImagesEnabled: false
|
||||
property bool requestActive: false
|
||||
property int requestSerial: 0
|
||||
property int applySerial: 0
|
||||
@@ -51,6 +52,9 @@ Item {
|
||||
property int sliceSpacing: -30
|
||||
property int skewOffset: 28
|
||||
property int bottomChromeHeight: showLabels ? (filterable ? 104 : 74) : (filterable ? 60 : 30)
|
||||
// Render only what fits on this display, plus one prefetch slice per side.
|
||||
readonly property int previewRadius: Math.max(1, Math.min(16, Math.ceil((panel.width - expandedWidth) / (2 * (sliceWidth + sliceSpacing))) + 1))
|
||||
onPreviewRadiusChanged: updateVisibleItems()
|
||||
|
||||
onOpenedChanged: if (!opened) layoutSettled = false
|
||||
|
||||
@@ -100,14 +104,6 @@ Item {
|
||||
return ImagePickerModel.firstMatchingIndex(imageArray, filterText)
|
||||
}
|
||||
|
||||
function filteredPosition(index) {
|
||||
return ImagePickerModel.filteredPosition(imageArray, index, filterText)
|
||||
}
|
||||
|
||||
function selectedFilteredPosition() {
|
||||
return ImagePickerModel.selectedFilteredPosition(imageArray, selectedIndex, filterText)
|
||||
}
|
||||
|
||||
function select(index, immediate) {
|
||||
if (imageArray.length === 0) return
|
||||
if (index < 0) index = 0
|
||||
@@ -215,8 +211,10 @@ Item {
|
||||
|
||||
root.loadedImageRows = rows
|
||||
root.selectedIndex = root.indexForSelectedImage(newImages)
|
||||
root.neighborImagesEnabled = false
|
||||
root.imageArray = newImages
|
||||
root.imagesLoaded = true
|
||||
Qt.callLater(root.enableNeighborsWhenReady)
|
||||
|
||||
if (reveal !== false) {
|
||||
root.opened = true
|
||||
@@ -273,6 +271,22 @@ Item {
|
||||
}
|
||||
|
||||
property var imageArray: []
|
||||
readonly property var matchingImageIndices: ImagePickerModel.matchingIndices(imageArray, filterText)
|
||||
onMatchingImageIndicesChanged: updateVisibleItems()
|
||||
onSelectedIndexChanged: updateVisibleItems()
|
||||
|
||||
function updateVisibleItems() {
|
||||
ImagePickerModel.syncWindow(visibleImages, ImagePickerModel.visibleWindow(matchingImageIndices, selectedIndex, previewRadius))
|
||||
}
|
||||
|
||||
function enableNeighborsWhenReady() {
|
||||
for (var i = 0; i < imageCards.count; i++) {
|
||||
var item = imageCards.itemAt(i)
|
||||
if (item && item.selected && item.previewReady) neighborImagesEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
ListModel { id: visibleImages }
|
||||
|
||||
|
||||
function currentThemePreview() {
|
||||
@@ -465,6 +479,8 @@ Item {
|
||||
Item {
|
||||
id: card
|
||||
visible: root.opened && root.imagesLoaded && root.layoutSettled && root.imageArray.length > 0
|
||||
opacity: root.layoutSettled ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: 90 } }
|
||||
width: Math.min(parent.width - 80, root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) + 40)
|
||||
height: root.expandedHeight + Style.space(30) + root.bottomChromeHeight
|
||||
anchors.centerIn: parent
|
||||
@@ -515,30 +531,32 @@ Item {
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
|
||||
Repeater {
|
||||
model: root.imageArray.length
|
||||
id: imageCards
|
||||
model: visibleImages
|
||||
|
||||
delegate: Item {
|
||||
id: item
|
||||
required property int index
|
||||
required property int imageIndex
|
||||
required property int relativeIndex
|
||||
|
||||
readonly property var imageData: root.imageArray[index]
|
||||
readonly property var imageData: root.imageArray[imageIndex]
|
||||
readonly property string filePath: imageData ? imageData.filePath : ""
|
||||
readonly property string fileName: imageData ? imageData.fileName : ""
|
||||
readonly property string thumbnailPath: imageData ? imageData.thumbnailPath : ""
|
||||
|
||||
readonly property bool matched: root.itemMatches(index)
|
||||
readonly property int relativeIndex: root.filteredPosition(index) - root.selectedFilteredPosition()
|
||||
readonly property bool selected: matched && index === root.selectedIndex
|
||||
readonly property bool nearby: matched && Math.abs(relativeIndex) <= 16
|
||||
property bool sourceActivated: nearby
|
||||
onNearbyChanged: if (nearby) sourceActivated = true
|
||||
readonly property bool selected: imageIndex === root.selectedIndex
|
||||
readonly property bool previewReady: image.status === Image.Ready || image.status === Image.Error
|
||||
onSelectedChanged: if (selected && previewReady) root.neighborImagesEnabled = true
|
||||
|
||||
visible: nearby
|
||||
x: selected ? carousel.previewX : (relativeIndex < 0 ? carousel.previewX + relativeIndex * carousel.itemStep : carousel.previewX + root.expandedWidth + root.sliceSpacing + (relativeIndex - 1) * carousel.itemStep)
|
||||
width: selected ? root.expandedWidth : root.sliceWidth
|
||||
height: selected ? root.expandedHeight : root.sliceHeight
|
||||
y: selected ? 0 : (root.expandedHeight - root.sliceHeight) / 2
|
||||
z: selected ? 100 : 50 - Math.min(Math.abs(relativeIndex), 40)
|
||||
Behavior on x { NumberAnimation { duration: 90; easing.type: Easing.OutCubic } }
|
||||
Behavior on y { NumberAnimation { duration: 90; easing.type: Easing.OutCubic } }
|
||||
Behavior on width { NumberAnimation { duration: 90; easing.type: Easing.OutCubic } }
|
||||
Behavior on height { NumberAnimation { duration: 90; easing.type: Easing.OutCubic } }
|
||||
|
||||
readonly property real skAbs: Math.abs(root.skewOffset)
|
||||
readonly property real topLeft: root.skewOffset >= 0 ? skAbs : 0
|
||||
@@ -579,17 +597,25 @@ Item {
|
||||
maskSpreadAtMin: 0.3
|
||||
}
|
||||
|
||||
Rectangle { anchors.fill: parent; color: root.dimColor }
|
||||
|
||||
Image {
|
||||
id: image
|
||||
anchors.fill: parent
|
||||
// Load only the initial/visited nearby images, but keep the
|
||||
// source once activated so Qt does not tear textures down as
|
||||
// selection moves through the carousel.
|
||||
source: item.sourceActivated && item.thumbnailPath ? Util.fileUrl(item.thumbnailPath) : ""
|
||||
// Even an uncached 6K wallpaper decodes at card size, off the
|
||||
// GUI thread. Departing cards release their images instead of
|
||||
// retaining every preview visited in a large collection.
|
||||
// Queue the selected preview first; neighbors must not delay
|
||||
// the image the user opened the picker to see.
|
||||
source: (item.selected || root.neighborImagesEnabled) && item.thumbnailPath ? Util.fileUrl(item.thumbnailPath) : ""
|
||||
sourceSize: Qt.size(root.expandedWidth, root.expandedHeight)
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: false
|
||||
cache: true
|
||||
asynchronous: true
|
||||
cache: false
|
||||
smooth: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: 90 } }
|
||||
onStatusChanged: if (item.selected && (status === Image.Ready || status === Image.Error)) root.neighborImagesEnabled = true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -617,7 +643,7 @@ Item {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: item.selected ? root.applySelected() : root.select(index)
|
||||
onClicked: item.selected ? root.applySelected() : root.select(item.imageIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,45 @@ function nextSelectedIndexForFilter(images, selectedIndex, filterText) {
|
||||
return firstMatchingIndex(images, filterText)
|
||||
}
|
||||
|
||||
function matchingIndices(images, filterText) {
|
||||
var indices = []
|
||||
for (var i = 0; i < images.length; i++) {
|
||||
if (itemMatches(images, i, filterText)) indices.push(i)
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
// Keep the rendered carousel independent of the size of the collection.
|
||||
function visibleWindow(indices, selectedIndex, radius) {
|
||||
var position = indices.indexOf(selectedIndex)
|
||||
if (position < 0) position = 0
|
||||
var items = []
|
||||
for (var i = Math.max(0, position - radius); i < Math.min(indices.length, position + radius + 1); i++) {
|
||||
items.push({ imageIndex: indices[i], relativeIndex: i - position })
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Preserve overlapping delegates as selection moves, including their decoded
|
||||
// images. Replacing the entire model on every keypress makes previews flicker.
|
||||
function syncWindow(model, items) {
|
||||
var wanted = {}
|
||||
for (var i = 0; i < items.length; i++) wanted[items[i].imageIndex] = true
|
||||
for (var i = model.count - 1; i >= 0; i--) {
|
||||
if (!wanted[model.get(i).imageIndex]) model.remove(i)
|
||||
}
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var existing = i
|
||||
while (existing < model.count && model.get(existing).imageIndex !== items[i].imageIndex) existing++
|
||||
if (existing === model.count) {
|
||||
model.insert(i, items[i])
|
||||
} else {
|
||||
if (existing !== i) model.move(existing, i, 1)
|
||||
model.setProperty(i, "relativeIndex", items[i].relativeIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
nameForPath: nameForPath,
|
||||
@@ -92,6 +131,9 @@ if (typeof module !== "undefined") {
|
||||
filteredPosition: filteredPosition,
|
||||
selectedFilteredPosition: selectedFilteredPosition,
|
||||
indexForSelectedImage: indexForSelectedImage,
|
||||
nextSelectedIndexForFilter: nextSelectedIndexForFilter
|
||||
nextSelectedIndexForFilter: nextSelectedIndexForFilter,
|
||||
matchingIndices: matchingIndices,
|
||||
visibleWindow: visibleWindow,
|
||||
syncWindow: syncWindow
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user