Qt 6.12 shadows the bare Color singleton, so the bar and every Color role read undefined on fresh installs. Keep our I18n text in the four conflicting files, qualify #14433's notification button color, give the new dictation menu rows and notifications Chinese, and drop table entries for the removed Voxtype and FIDO2 texts.
715 lines
24 KiB
QML
715 lines
24 KiB
QML
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import QtQuick.Effects
|
|
import QtQuick.Shapes
|
|
import qs.Commons
|
|
import qs.Commons as Commons
|
|
import qs.Ui
|
|
import "ImagePickerModel.js" as ImagePickerModel
|
|
|
|
Item {
|
|
id: root
|
|
|
|
// Injected by omarchy-shell; defaults to the session OMARCHY_PATH.
|
|
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
|
property string stateHome: Quickshell.env("HOME") + "/.local/state"
|
|
property string imageDirs: Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIRS") || Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIR") || Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (stateHome + "/omarchy/current/theme/backgrounds")
|
|
property string imageRows: ""
|
|
property string loadedImageRows: ""
|
|
property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_BACKGROUND_SELECTION_FILE")
|
|
property string selectedImage: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTED")
|
|
property int selectedIndex: 0
|
|
property bool imagesLoaded: false
|
|
property bool opened: false
|
|
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
|
|
property string doneFile: ""
|
|
property string filterText: ""
|
|
property var doneFilesToRelease: []
|
|
// Themes open from rows the shell already holds, so the picker shows without
|
|
// waiting on omarchy-theme-switcher; each open refreshes them behind it.
|
|
property string themeRows: ""
|
|
property bool themeMode: false
|
|
property bool themeOpenPending: false
|
|
// Bound to the central [image-picker] section in shell.toml via Commons.Color.qml.
|
|
// `dimColor` tints unselected slices and text outlines on top of the scrim;
|
|
// it intentionally tracks the foundational background, not a surface role.
|
|
property color dimColor: Commons.Color.background
|
|
property color foreground: Commons.Color.imagePicker.text
|
|
property color scrim: Commons.Color.imagePicker.scrim
|
|
property color selectedBorder: Commons.Color.imagePicker.selectedBorder
|
|
property color unselectedBorder: Commons.Color.imagePicker.unselectedBorder
|
|
property int expandedWidth: 768
|
|
property int expandedHeight: 475
|
|
property int sliceWidth: 108
|
|
property int sliceHeight: 432
|
|
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
|
|
|
|
function scriptPath(name) {
|
|
return omarchyPath + "/shell/plugins/image-picker/" + name
|
|
}
|
|
|
|
function focusPicker() {
|
|
if (root.opened && root.imagesLoaded && root.layoutSettled)
|
|
carousel.forceActiveFocus()
|
|
}
|
|
|
|
function revealWhenSettled(serial) {
|
|
Qt.callLater(function() {
|
|
if (serial === root.requestSerial && root.opened && root.imagesLoaded && root.imageArray.length > 0) {
|
|
root.layoutSettled = true
|
|
root.focusPicker()
|
|
}
|
|
})
|
|
}
|
|
|
|
function currentPath() {
|
|
if (imageArray.length === 0 || !itemMatches(selectedIndex)) return ""
|
|
return imageArray[selectedIndex].filePath
|
|
}
|
|
|
|
function nameForPath(path) {
|
|
return ImagePickerModel.nameForPath(path)
|
|
}
|
|
|
|
function labelForPath(path) {
|
|
return ImagePickerModel.labelForPath(path)
|
|
}
|
|
|
|
function currentLabel() {
|
|
var path = currentPath()
|
|
if (!path) return filterText ? I18n.tr("No matches") : ""
|
|
|
|
return labelForPath(path)
|
|
}
|
|
|
|
function itemMatches(index) {
|
|
return ImagePickerModel.itemMatches(imageArray, index, filterText)
|
|
}
|
|
|
|
function firstMatchingIndex() {
|
|
return ImagePickerModel.firstMatchingIndex(imageArray, filterText)
|
|
}
|
|
|
|
function select(index, immediate) {
|
|
if (imageArray.length === 0) return
|
|
if (index < 0) index = 0
|
|
else if (index >= imageArray.length) index = imageArray.length - 1
|
|
if (!itemMatches(index)) return
|
|
if (index === selectedIndex && immediate !== true) return
|
|
|
|
selectedIndex = index
|
|
}
|
|
|
|
function selectAdjacent(direction) {
|
|
var count = imageArray.length
|
|
if (count === 0) return
|
|
|
|
var index = selectedIndex
|
|
for (var i = 0; i < count; i++) {
|
|
index = (index + direction + count) % count
|
|
if (itemMatches(index)) {
|
|
select(index)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateFilter(nextFilterText) {
|
|
filterText = nextFilterText
|
|
|
|
if (!itemMatches(selectedIndex)) {
|
|
var first = ImagePickerModel.nextSelectedIndexForFilter(imageArray, selectedIndex, filterText)
|
|
if (first >= 0) selectedIndex = first
|
|
}
|
|
}
|
|
|
|
function releaseNextDoneFile() {
|
|
if (releaseProc.running || doneFilesToRelease.length === 0) return
|
|
|
|
var path = doneFilesToRelease.shift()
|
|
releaseProc.command = ["bash", "-c", ": > " + Util.shellQuote(path)]
|
|
releaseProc.running = true
|
|
}
|
|
|
|
function finishDoneFile(path) {
|
|
if (!path) return
|
|
doneFilesToRelease.push(path)
|
|
releaseNextDoneFile()
|
|
}
|
|
|
|
function applySelected() {
|
|
var path = currentPath()
|
|
|
|
if (themeMode) {
|
|
themeMode = false
|
|
root.opened = false
|
|
if (path) Util.execArgv(["omarchy-theme-set", nameForPath(path)])
|
|
return
|
|
}
|
|
|
|
if (!path || !selectionFile) {
|
|
cancel()
|
|
return
|
|
}
|
|
|
|
var activeSelectionFile = selectionFile
|
|
var activeDoneFile = doneFile
|
|
applySerial = requestSerial
|
|
requestActive = false
|
|
selectionFile = ""
|
|
doneFile = ""
|
|
|
|
applyProc.command = ["bash", "-c", "printf '%s\\n' " + Util.shellQuote(path) + " > " + Util.shellQuote(activeSelectionFile) + "; : > " + Util.shellQuote(activeDoneFile)]
|
|
applyProc.running = true
|
|
}
|
|
|
|
function cancel() {
|
|
themeOpenPending = false
|
|
|
|
if (requestActive)
|
|
finishDoneFile(doneFile)
|
|
|
|
requestActive = false
|
|
selectionFile = ""
|
|
doneFile = ""
|
|
root.opened = false
|
|
}
|
|
|
|
function closeSelector(nextDoneFile) {
|
|
requestSerial += 1
|
|
themeOpenPending = false
|
|
|
|
if (requestActive)
|
|
finishDoneFile(doneFile)
|
|
|
|
if (nextDoneFile && nextDoneFile !== doneFile)
|
|
finishDoneFile(nextDoneFile)
|
|
|
|
requestActive = false
|
|
selectionFile = ""
|
|
doneFile = ""
|
|
filterText = ""
|
|
root.opened = false
|
|
}
|
|
|
|
function loadRows(rows, reveal) {
|
|
var newImages = ImagePickerModel.loadRows(rows)
|
|
|
|
root.loadedImageRows = rows
|
|
var nextIndex = root.indexForSelectedImage(newImages)
|
|
root.selectedIndex = ImagePickerModel.nextSelectedIndexForFilter(newImages, nextIndex, root.filterText)
|
|
root.neighborImagesEnabled = false
|
|
root.imageArray = newImages
|
|
root.imagesLoaded = true
|
|
Qt.callLater(root.enableNeighborsWhenReady)
|
|
|
|
if (reveal !== false) {
|
|
root.opened = true
|
|
root.revealWhenSettled(root.requestSerial)
|
|
}
|
|
}
|
|
|
|
function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextShowLabels, nextFilterable) {
|
|
if (requestActive && doneFile && doneFile !== nextDoneFile)
|
|
finishDoneFile(doneFile)
|
|
|
|
requestSerial += 1
|
|
themeMode = false
|
|
themeOpenPending = false
|
|
|
|
imageDirs = nextImageDirs
|
|
imageRows = nextImageRows
|
|
selectedImage = nextSelectedImage
|
|
selectionFile = nextSelectionFile
|
|
doneFile = nextDoneFile
|
|
requestActive = !!doneFile
|
|
showLabels = nextShowLabels === true || nextShowLabels === "true"
|
|
filterable = nextFilterable === true || nextFilterable === "true"
|
|
filterText = ""
|
|
layoutSettled = false
|
|
|
|
if (imageRows && imageRows === loadedImageRows && imageArray.length > 0) {
|
|
root.select(root.selectedImageIndex(), true)
|
|
imagesLoaded = true
|
|
opened = true
|
|
root.revealWhenSettled(requestSerial)
|
|
return
|
|
}
|
|
|
|
if (imageRows) {
|
|
var rowsToLoad = imageRows
|
|
var rowsSerial = requestSerial
|
|
imageArray = []
|
|
selectedIndex = 0
|
|
imagesLoaded = true
|
|
opened = true
|
|
Qt.callLater(function() {
|
|
if (rowsSerial === root.requestSerial)
|
|
root.loadRows(rowsToLoad, true)
|
|
})
|
|
return
|
|
}
|
|
|
|
imageArray = []
|
|
selectedIndex = 0
|
|
imagesLoaded = false
|
|
opened = false
|
|
startImageScan(requestSerial, imageDirs)
|
|
}
|
|
|
|
property var imageArray: []
|
|
readonly property var matchingImageIndices: ImagePickerModel.matchingIndices(imageArray, filterText)
|
|
onMatchingImageIndicesChanged: updateVisibleItems()
|
|
onSelectedIndexChanged: {
|
|
updateVisibleItems()
|
|
introPrepareTimer.restart()
|
|
}
|
|
onThemeModeChanged: if (themeMode) introPrepareTimer.restart()
|
|
|
|
Timer {
|
|
id: introPrepareTimer
|
|
interval: 75
|
|
onTriggered: {
|
|
if (root.opened && root.themeMode && !introPrepare.running) {
|
|
introPrepare.theme = root.nameForPath(root.currentPath())
|
|
introPrepare.command = ["omarchy-theme-bg-boot-intro", "--prepare-theme", root.nameForPath(root.currentPath())]
|
|
introPrepare.running = true
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: introPrepare
|
|
property string theme: ""
|
|
onExited: if (root.opened && root.themeMode && theme !== root.nameForPath(root.currentPath())) introPrepareTimer.restart()
|
|
}
|
|
|
|
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() {
|
|
var name = String(themeNameFile.text() || "").trim()
|
|
var images = ImagePickerModel.loadRows(themeRows)
|
|
for (var i = 0; i < images.length; i++) {
|
|
if (nameForPath(images[i].filePath) === name) return images[i].filePath
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function openThemes() {
|
|
if (themeRows) {
|
|
openThemeRows()
|
|
} else {
|
|
// First open before the startup refresh has landed.
|
|
themeOpenPending = true
|
|
}
|
|
refreshThemeRows()
|
|
}
|
|
|
|
function openThemeRows() {
|
|
openSelector("", themeRows, currentThemePreview(), "", "", true, true)
|
|
themeMode = true
|
|
}
|
|
|
|
function refreshThemeRows() {
|
|
if (!themeRowsProc.running) themeRowsProc.running = true
|
|
}
|
|
|
|
function updateThemeRows(rows) {
|
|
var changed = rows !== themeRows
|
|
themeRows = rows
|
|
|
|
if (themeOpenPending) {
|
|
themeOpenPending = false
|
|
if (rows) openThemeRows()
|
|
} else if (changed && rows && themeMode && opened) {
|
|
// A theme was added or removed since the rows were last read. Keep the
|
|
// user's place in the carousel rather than jumping back to the current.
|
|
selectedImage = currentPath() || currentThemePreview()
|
|
imageRows = rows
|
|
loadRows(rows, false)
|
|
}
|
|
}
|
|
|
|
FileView {
|
|
id: themeNameFile
|
|
path: root.stateHome + "/omarchy/current/theme.name"
|
|
watchChanges: true
|
|
onFileChanged: reload()
|
|
}
|
|
|
|
Process {
|
|
id: themeRowsProc
|
|
command: [root.omarchyPath + "/bin/omarchy-theme-switcher", "--print-rows"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.updateThemeRows(String(text || "").trim())
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: refreshThemeRows()
|
|
|
|
function startImageScan(serial, dirs) {
|
|
if (loadImagesProc.running) {
|
|
loadImagesProc.queuedSerial = serial
|
|
loadImagesProc.queuedDirs = dirs
|
|
return
|
|
}
|
|
|
|
loadImagesProc.activeSerial = serial
|
|
loadImagesProc.queuedSerial = 0
|
|
loadImagesProc.queuedDirs = ""
|
|
loadImagesProc.command = [root.scriptPath("list.sh"), dirs]
|
|
loadImagesProc.running = true
|
|
}
|
|
|
|
function indexForSelectedImage(images) {
|
|
return ImagePickerModel.indexForSelectedImage(images, selectedImage)
|
|
}
|
|
|
|
function selectedImageIndex() {
|
|
return indexForSelectedImage(imageArray)
|
|
}
|
|
|
|
Process {
|
|
id: loadImagesProc
|
|
property int activeSerial: 0
|
|
property int queuedSerial: 0
|
|
property string queuedDirs: ""
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: {
|
|
if (loadImagesProc.activeSerial === root.requestSerial)
|
|
root.loadRows(String(text || ""), true)
|
|
}
|
|
}
|
|
onExited: {
|
|
var serial = queuedSerial
|
|
var dirs = queuedDirs
|
|
activeSerial = 0
|
|
queuedSerial = 0
|
|
queuedDirs = ""
|
|
if (serial > 0 && serial === root.requestSerial)
|
|
root.startImageScan(serial, dirs)
|
|
}
|
|
}
|
|
|
|
// Lifecycle hooks invoked by omarchy-shell summon/hide. shell.summon(id,
|
|
// payloadJson) hands the JSON to open() here; shell.hide(id) calls close().
|
|
// The shell host owns the stable `image-selector` IPC target and forwards
|
|
// those lower-level positional calls here.
|
|
function open(payload) {
|
|
var args = {}
|
|
if (payload) {
|
|
try { args = JSON.parse(payload) || {} } catch (e) { args = {} }
|
|
}
|
|
if (args.source === "themes") {
|
|
openThemes()
|
|
return
|
|
}
|
|
var dirs = String(args.imageDirs || imageDirs)
|
|
var rows = String(args.imageRows || "")
|
|
var sel = String(args.selectedImage || selectedImage)
|
|
var selFile = String(args.selectionFile || "")
|
|
var doneF = String(args.doneFile || "")
|
|
var labels = args.showLabels === true || args.showLabels === "true"
|
|
var filter = args.filterable === true || args.filterable === "true"
|
|
openSelector(dirs, rows, sel, selFile, doneF, labels, filter)
|
|
}
|
|
|
|
function close() {
|
|
cancel()
|
|
}
|
|
|
|
function preloadRows(nextImageRows, nextSelectedImage, nextShowLabels, nextFilterable) {
|
|
// Theme/background set hooks can warm selector rows after a picker was
|
|
// dismissed. Ignore those preloads while a user-visible request is open;
|
|
// otherwise the preload resets layoutSettled without revealing again,
|
|
// leaving only the fullscreen scrim.
|
|
if (opened || requestActive) return
|
|
|
|
requestSerial += 1
|
|
imageRows = nextImageRows
|
|
selectedImage = nextSelectedImage
|
|
showLabels = nextShowLabels === true || nextShowLabels === "true"
|
|
filterable = nextFilterable === true || nextFilterable === "true"
|
|
filterText = ""
|
|
layoutSettled = false
|
|
|
|
if (imageRows && imageRows === loadedImageRows && imageArray.length > 0) {
|
|
selectedIndex = selectedImageIndex()
|
|
imagesLoaded = true
|
|
} else if (imageRows) {
|
|
loadRows(imageRows, false)
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: applyProc
|
|
onExited: {
|
|
if (root.applySerial === root.requestSerial)
|
|
root.opened = false
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: releaseProc
|
|
onExited: root.releaseNextDoneFile()
|
|
}
|
|
|
|
OverlayWindow {
|
|
id: panel
|
|
shown: root.opened
|
|
shownKeyboardFocus: root.imagesLoaded ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
|
WlrLayershell.namespace: "omarchy-image-selector"
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
visible: root.opened && root.imagesLoaded
|
|
color: root.scrim
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
enabled: root.opened && root.imagesLoaded
|
|
onClicked: root.cancel()
|
|
}
|
|
|
|
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
|
|
|
|
MouseArea { anchors.fill: parent; onClicked: {} }
|
|
|
|
Item {
|
|
id: carousel
|
|
anchors.top: parent.top
|
|
anchors.topMargin: Style.space(30)
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: root.bottomChromeHeight
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
width: root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing)
|
|
clip: false
|
|
focus: true
|
|
|
|
readonly property real itemStep: root.sliceWidth + root.sliceSpacing
|
|
readonly property real previewX: (width - root.expandedWidth) / 2
|
|
|
|
Keys.priority: Keys.BeforeItem
|
|
Keys.onPressed: function(event) {
|
|
if (event.key === Qt.Key_Escape) {
|
|
if (root.filterText) {
|
|
root.updateFilter("")
|
|
} else {
|
|
root.cancel()
|
|
}
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
root.applySelected()
|
|
event.accepted = true
|
|
} else if (root.filterable && Util.editsFilter(event, root.filterText)) {
|
|
root.updateFilter(Util.editedFilter(event, root.filterText))
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Left || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier) || event.key === Qt.Key_Backtab) {
|
|
root.selectAdjacent(-1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_Tab) {
|
|
root.selectAdjacent(1)
|
|
event.accepted = true
|
|
} else if (root.filterable && event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) {
|
|
root.updateFilter(root.filterText + event.text)
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: forceActiveFocus()
|
|
|
|
Repeater {
|
|
id: imageCards
|
|
model: visibleImages
|
|
|
|
delegate: Item {
|
|
id: item
|
|
required property int imageIndex
|
|
required property int relativeIndex
|
|
|
|
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 selected: imageIndex === root.selectedIndex
|
|
readonly property bool previewReady: image.status === Image.Ready || image.status === Image.Error
|
|
onSelectedChanged: if (selected && previewReady) root.neighborImagesEnabled = true
|
|
|
|
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
|
|
readonly property real topRight: root.skewOffset >= 0 ? width : width - skAbs
|
|
readonly property real bottomRight: root.skewOffset >= 0 ? width - skAbs : width
|
|
readonly property real bottomLeft: root.skewOffset >= 0 ? 0 : skAbs
|
|
|
|
Item {
|
|
id: maskShape
|
|
anchors.fill: parent
|
|
visible: false
|
|
layer.enabled: true
|
|
|
|
Shape {
|
|
anchors.fill: parent
|
|
antialiasing: true
|
|
preferredRendererType: Shape.CurveRenderer
|
|
ShapePath {
|
|
fillColor: "white"
|
|
strokeColor: "transparent"
|
|
startX: item.topLeft; startY: 0
|
|
PathLine { x: item.topRight; y: 0 }
|
|
PathLine { x: item.bottomRight; y: item.height }
|
|
PathLine { x: item.bottomLeft; y: item.height }
|
|
PathLine { x: item.topLeft; y: 0 }
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
layer.enabled: true
|
|
layer.smooth: true
|
|
layer.effect: MultiEffect {
|
|
maskEnabled: true
|
|
maskSource: maskShape
|
|
maskThresholdMin: 0.3
|
|
maskSpreadAtMin: 0.3
|
|
}
|
|
|
|
Rectangle { anchors.fill: parent; color: root.dimColor }
|
|
|
|
Image {
|
|
id: image
|
|
anchors.fill: parent
|
|
// Decode at the expanded card's physical size, off the GUI
|
|
// thread. Keep that size during navigation to avoid reloads.
|
|
// 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.width: Math.ceil(root.expandedWidth * Screen.devicePixelRatio)
|
|
sourceSize.height: Math.ceil(root.expandedHeight * Screen.devicePixelRatio)
|
|
fillMode: Image.PreserveAspectCrop
|
|
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 {
|
|
anchors.fill: parent
|
|
color: Util.alpha(root.dimColor, item.selected ? 0 : 0.42)
|
|
}
|
|
}
|
|
|
|
Shape {
|
|
anchors.fill: parent
|
|
antialiasing: true
|
|
preferredRendererType: Shape.CurveRenderer
|
|
ShapePath {
|
|
fillColor: "transparent"
|
|
strokeColor: item.selected ? root.selectedBorder : root.unselectedBorder
|
|
strokeWidth: item.selected ? 3 : 1
|
|
startX: item.topLeft; startY: 0
|
|
PathLine { x: item.topRight; y: 0 }
|
|
PathLine { x: item.bottomRight; y: item.height }
|
|
PathLine { x: item.bottomLeft; y: item.height }
|
|
PathLine { x: item.topLeft; y: 0 }
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: item.selected ? root.applySelected() : root.select(item.imageIndex)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
id: selectedLabel
|
|
textFormat: Text.PlainText
|
|
visible: root.showLabels
|
|
anchors.top: carousel.bottom
|
|
anchors.topMargin: Style.space(16)
|
|
anchors.horizontalCenter: carousel.horizontalCenter
|
|
width: root.expandedWidth
|
|
text: root.currentLabel()
|
|
color: root.foreground
|
|
style: Text.Outline
|
|
styleColor: Util.alpha(root.dimColor, 0.7)
|
|
font.pixelSize: Style.font.display
|
|
font.weight: Font.DemiBold
|
|
horizontalAlignment: Text.AlignHCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
visible: root.filterable && root.filterText
|
|
anchors.top: selectedLabel.bottom
|
|
anchors.topMargin: Style.space(8)
|
|
anchors.horizontalCenter: carousel.horizontalCenter
|
|
width: root.expandedWidth
|
|
text: root.filterText
|
|
color: root.foreground
|
|
opacity: 0.85
|
|
style: Text.Outline
|
|
styleColor: Util.alpha(root.dimColor, 0.7)
|
|
font.pixelSize: Style.font.title
|
|
horizontalAlignment: Text.AlignHCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|