* Run menu summon actions in-process A menu action that only summons another shell plugin spawned bash and a qs ipc client to ask this same shell to do it, about 60ms of the path. Call shell.summon directly instead, and fall back to bash when the call is refused or the action is anything more than a bare summon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Open the theme picker from rows held in the shell Opening the theme picker ran omarchy-theme-switcher to rebuild its index and then made a second IPC call, about 170ms before the picker mapped. The picker now holds the theme rows itself, opens from them at once, and refreshes them behind the open via omarchy-theme-switcher --print-rows. It applies the chosen theme with omarchy-theme-set directly, so omarchy-theme-set no longer preloads the picker. From the keybinding to the overlay mapped drops from ~245ms to ~83ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the image picker surface mapped between opens Each open mapped a fresh surface, which rendered its first frames before Hyprland sent its fractional scale: the pixel ratio stepped 2, 1, then 1.6, so the picker flashed blurry for ~130ms and re-uploaded every thumbnail texture. Keep the surface and park it transparent and input-less on the bottom layer while closed, since anything on the overlay layer blocks direct scanout for fullscreen apps. It follows the focused monitor on each open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
689 lines
23 KiB
QML
689 lines
23 KiB
QML
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import QtQuick.Effects
|
|
import QtQuick.Shapes
|
|
import qs.Commons
|
|
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 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
|
|
// The surface outlives each open, so it no longer lands on the focused
|
|
// output by itself. Unset until the first open lets the compositor choose.
|
|
property var targetScreen: null
|
|
// Bound to the central [image-picker] section in shell.toml via 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: Color.background
|
|
property color foreground: Color.imagePicker.text
|
|
property color scrim: Color.imagePicker.scrim
|
|
property color selectedBorder: Color.imagePicker.selectedBorder
|
|
property color unselectedBorder: 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)
|
|
|
|
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 ? "No matches" : ""
|
|
|
|
return labelForPath(path)
|
|
}
|
|
|
|
function itemMatches(index) {
|
|
return ImagePickerModel.itemMatches(imageArray, index, filterText)
|
|
}
|
|
|
|
function firstMatchingIndex() {
|
|
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
|
|
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
|
|
root.selectedIndex = root.indexForSelectedImage(newImages)
|
|
root.imageArray = newImages
|
|
root.imagesLoaded = true
|
|
|
|
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
|
|
targetScreen = focusedScreen() || targetScreen
|
|
|
|
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: []
|
|
|
|
function focusedScreen() {
|
|
var monitor = Hyprland.focusedMonitor
|
|
var name = monitor ? String(monitor.name || "") : ""
|
|
for (var i = 0; i < Quickshell.screens.length; i++) {
|
|
if (Quickshell.screens[i].name === name) return Quickshell.screens[i]
|
|
}
|
|
return null
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
PanelWindow {
|
|
id: panel
|
|
|
|
// Stay mapped between opens. A fresh surface renders its first frames
|
|
// before the compositor sends its fractional scale, so the picker flashed
|
|
// blurry, and it had to re-upload every thumbnail texture. Closed, it
|
|
// waits transparent and input-less on the bottom layer: anything left on
|
|
// the overlay layer would block direct scanout for fullscreen apps.
|
|
visible: true
|
|
screen: root.targetScreen
|
|
anchors { top: true; bottom: true; left: true; right: true }
|
|
color: "transparent"
|
|
mask: root.opened ? null : closedMask
|
|
WlrLayershell.namespace: "omarchy-image-selector"
|
|
WlrLayershell.layer: root.opened ? WlrLayer.Overlay : WlrLayer.Bottom
|
|
WlrLayershell.keyboardFocus: root.opened && root.imagesLoaded ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
Region { id: closedMask }
|
|
|
|
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
|
|
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 {
|
|
model: root.imageArray.length
|
|
|
|
delegate: Item {
|
|
id: item
|
|
required property int index
|
|
|
|
readonly property var imageData: root.imageArray[index]
|
|
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
|
|
|
|
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)
|
|
|
|
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
|
|
}
|
|
|
|
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) : ""
|
|
fillMode: Image.PreserveAspectCrop
|
|
asynchronous: false
|
|
cache: true
|
|
smooth: 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(index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|