mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-15 15:54:00 -04:00
cc: add trash icon to drag and crush things cc: allow anchoring footer items to start or end
575 lines
24 KiB
QML
575 lines
24 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Modules.ControlCenter
|
|
import qs.Modules.ControlCenter.Widgets
|
|
import qs.Widgets
|
|
import "../utils/widgets.js" as WidgetUtils
|
|
|
|
Item {
|
|
id: root
|
|
|
|
LayoutMirroring.enabled: I18n.isRtl
|
|
LayoutMirroring.childrenInherit: true
|
|
|
|
required property var grid
|
|
property bool editMode: false
|
|
property var items: []
|
|
property bool onTop: false
|
|
property var transientSurfaceTracker: null
|
|
// A grid tile hovering the row: {"cells", "end", "at"}, with `at` counted within the start or end group.
|
|
property var incoming: null
|
|
// Takes (savedIndex, cells, scenePoint) and returns true when it takes the dropped item.
|
|
property var dropHandler: null
|
|
// Takes the dragged item's scene rect and returns true while something outside the row will take it.
|
|
property var leavesRow: null
|
|
property int liftedIndex: -1
|
|
property var liftedSlot: null
|
|
property point liftedPoint: Qt.point(0, 0)
|
|
property bool gridDragging: false
|
|
property point gridDragPoint: Qt.point(0, 0)
|
|
// Set from the pointer, not bound: the slot reads the widths that the incoming tile changes.
|
|
property var gridSlot: null
|
|
property var resizePreview: null
|
|
property Item flyingItem: null
|
|
readonly property bool dragging: gridDragging || liftedIndex >= 0
|
|
readonly property bool overTrash: trashContains(liftedIndex >= 0 ? liftedPoint : gridDragPoint)
|
|
|
|
signal addWidgetRequested
|
|
signal resetRequested
|
|
signal clearRequested
|
|
signal moveRequested
|
|
signal editToggled
|
|
signal cancelRequested
|
|
signal removeRequested(int index)
|
|
signal configRequested(int index, var widgetData, var anchor)
|
|
signal itemMoved(int index, rect sceneRect, bool leaving)
|
|
signal resized(int index, int cells, bool fill)
|
|
|
|
// Grid columns, so footer widths line up with the tiles above.
|
|
readonly property real spacing: CcMetrics.gridGap
|
|
readonly property real pitch: grid.cellWidth
|
|
readonly property real editActionsWidth: Theme.iconButtonSize * 2 + spacing
|
|
readonly property real trackWidth: Math.max(0, width - trailing.width - CcMetrics.footerGap)
|
|
// Edit mode shows the whole row scaled into the room beside the edit buttons, so every width stays reachable.
|
|
readonly property real editScale: trackWidth > 0 ? Math.min(1, (trackWidth - editActionsWidth - CcMetrics.footerGap) / trackWidth) : 1
|
|
readonly property real trackScale: editMode ? editScale : 1
|
|
readonly property int capacity: Math.max(0, Math.floor((trackWidth + spacing) / pitch))
|
|
readonly property var fills: items.map(item => resizePreview?.index === item.index ? resizePreview.fill : WidgetUtils.footerFills(item.widget))
|
|
readonly property var mins: items.map(item => WidgetUtils.footerMinCells(item.widget.id))
|
|
readonly property var ends: items.map(item => WidgetUtils.footerEnds(item.widget))
|
|
// Fill items count at their minimum, so a drop or a neighbour growing takes room from them first.
|
|
readonly property var fixedCells: WidgetUtils.fitFooterCells(items.map((item, i) => fills[i] ? mins[i] : (resizePreview?.index === item.index ? resizePreview.cells : WidgetUtils.footerCells(item.widget))), mins, capacity)
|
|
readonly property int fixedUsed: fixedCells.reduce((sum, count) => sum + count, 0)
|
|
readonly property var cells: WidgetUtils.spreadFooterFill(fixedCells, fills, capacity - fixedUsed - (incoming?.cells ?? 0))
|
|
readonly property int slackIndex: fills.findIndex((fill, i) => fill && cells[i] > 0)
|
|
// The first fill item also takes the part of a cell left over, so it ends exactly at the next button.
|
|
readonly property real fillSlack: slackIndex < 0 ? 0 : Math.max(0, trackWidth + spacing - (cells.reduce((sum, count) => sum + count, 0) + (incoming?.cells ?? 0)) * pitch)
|
|
// Stable keys keep delegates alive across resizes and reorders, so they update in place instead of rebuilding.
|
|
readonly property var keys: {
|
|
const seen = {};
|
|
return items.map(item => {
|
|
const base = (item.widget.id ?? "") + "|" + (item.widget.instanceId ?? "");
|
|
seen[base] = (seen[base] ?? 0) + 1;
|
|
return base + "#" + seen[base];
|
|
});
|
|
}
|
|
readonly property var layout: {
|
|
const shown = groups();
|
|
const lifted = items.findIndex(item => item.index === liftedIndex);
|
|
const gap = lifted >= 0 ? liftedSlot : incoming;
|
|
const gapCells = lifted >= 0 ? cells[lifted] : (incoming?.cells ?? 0);
|
|
const endGapAt = gap?.end ? gap.at : -1;
|
|
const endFrom = trackWidth + spacing - groupWidth(shown.ends) - (endGapAt >= 0 ? gapCells * pitch : 0);
|
|
const start = placeGroup(shown.starts, 0, gap && !gap.end ? gap.at : -1, gapCells);
|
|
const end = placeGroup(shown.ends, endFrom, endGapAt, gapCells);
|
|
return {
|
|
"offsets": Object.assign(start.offsets, end.offsets),
|
|
"gapOffset": Math.max(start.gapOffset, end.gapOffset),
|
|
"gapCells": gapCells
|
|
};
|
|
}
|
|
|
|
implicitHeight: CcMetrics.footerHeight
|
|
|
|
onGridDragPointChanged: gridSlot = gridDragging ? insertionAt(gridDragPoint) : null
|
|
|
|
function spanWidth(count) {
|
|
return count * pitch - spacing;
|
|
}
|
|
|
|
function xFor(offset, span) {
|
|
return track.overhang + (I18n.isRtl ? trackWidth - offset - span : offset);
|
|
}
|
|
|
|
function freeCells() {
|
|
return capacity - fixedUsed;
|
|
}
|
|
|
|
function trashContains(scenePosition) {
|
|
if (!editMode || !dragging)
|
|
return false;
|
|
return trash.contains(trash.mapFromItem(null, scenePosition.x, scenePosition.y));
|
|
}
|
|
|
|
function containsScene(scenePosition) {
|
|
const point = mapFromItem(null, scenePosition.x, scenePosition.y);
|
|
return point.x >= 0 && point.x <= width && point.y >= -CcMetrics.gridGap && point.y <= height + CcMetrics.gridGap;
|
|
}
|
|
|
|
function spanAt(i) {
|
|
return cells[i] * pitch + (i === slackIndex ? fillSlack : 0);
|
|
}
|
|
|
|
function groupWidth(group) {
|
|
return group.reduce((sum, i) => sum + spanAt(i), 0);
|
|
}
|
|
|
|
// Shown items other than the lifted one, split into the start and end groups in saved order.
|
|
function groups() {
|
|
const shown = items.map((item, i) => i).filter(i => items[i].index !== liftedIndex && cells[i] > 0);
|
|
return {
|
|
"starts": shown.filter(i => !ends[i]),
|
|
"ends": shown.filter(i => ends[i])
|
|
};
|
|
}
|
|
|
|
function placeGroup(group, from, gapAt, gapCells) {
|
|
const offsets = [];
|
|
let gapOffset = -1;
|
|
let offset = from;
|
|
for (let k = 0; k <= group.length; k++) {
|
|
if (k === gapAt) {
|
|
gapOffset = offset;
|
|
offset += gapCells * pitch;
|
|
}
|
|
if (k === group.length)
|
|
break;
|
|
offsets[group[k]] = offset;
|
|
offset += spanAt(group[k]);
|
|
}
|
|
return {
|
|
"offsets": offsets,
|
|
"gapOffset": gapOffset
|
|
};
|
|
}
|
|
|
|
// The free space between the groups splits at its middle: the start half appends to the start group, the rest joins the end group.
|
|
function insertionAt(scenePosition) {
|
|
const local = track.mapFromItem(null, scenePosition.x, scenePosition.y).x - track.overhang;
|
|
const distance = I18n.isRtl ? trackWidth - local : local;
|
|
const shown = groups();
|
|
let offset = 0;
|
|
for (let k = 0; k < shown.starts.length; k++) {
|
|
const span = spanAt(shown.starts[k]);
|
|
if (distance < offset + span / 2)
|
|
return {
|
|
"end": false,
|
|
"at": k
|
|
};
|
|
offset += span;
|
|
}
|
|
const endFrom = trackWidth + spacing - groupWidth(shown.ends);
|
|
if (distance < (offset + endFrom) / 2)
|
|
return {
|
|
"end": false,
|
|
"at": shown.starts.length
|
|
};
|
|
offset = endFrom;
|
|
for (let k = 0; k < shown.ends.length; k++) {
|
|
const span = spanAt(shown.ends[k]);
|
|
if (distance < offset + span / 2)
|
|
return {
|
|
"end": true,
|
|
"at": k
|
|
};
|
|
offset += span;
|
|
}
|
|
return {
|
|
"end": true,
|
|
"at": shown.ends.length
|
|
};
|
|
}
|
|
|
|
// The saved index a drop at `slot` goes before, or -1 for the end of the list.
|
|
function savedIndexAt(slot) {
|
|
const group = groups()[slot.end ? "ends" : "starts"];
|
|
if (slot.at < group.length)
|
|
return items[group[slot.at]].index;
|
|
return group.length > 0 ? items[group[group.length - 1]].index + 1 : -1;
|
|
}
|
|
|
|
function slotOf(savedIndex) {
|
|
const at = items.findIndex(item => item.index === savedIndex);
|
|
const end = ends[at] ?? false;
|
|
return {
|
|
"end": end,
|
|
"at": groups()[end ? "ends" : "starts"].filter(i => i < at).length
|
|
};
|
|
}
|
|
|
|
Item {
|
|
id: track
|
|
|
|
// The layer only draws inside the item, so it reaches out far enough for the edit chrome.
|
|
readonly property real overhang: PopoutMetrics.editOverflow
|
|
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: -overhang
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: root.trackWidth + overhang * 2
|
|
height: CcMetrics.footerHeight + overhang * 2
|
|
// Scaled glyphs and icons alias; a mipmapped full-size layer shrinks them cleanly.
|
|
layer.enabled: trackScale.xScale < 1
|
|
layer.smooth: true
|
|
layer.mipmap: true
|
|
transform: Scale {
|
|
id: trackScale
|
|
|
|
origin.x: I18n.isRtl ? track.width - track.overhang : track.overhang
|
|
origin.y: track.height / 2
|
|
xScale: root.trackScale
|
|
yScale: root.trackScale
|
|
|
|
Behavior on xScale {
|
|
enabled: CcMetrics.animationsEnabled
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.expressiveEffects
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
|
}
|
|
}
|
|
|
|
Behavior on yScale {
|
|
enabled: CcMetrics.animationsEnabled
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.expressiveEffects
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
x: root.xFor(root.layout.gapOffset, root.spanWidth(root.layout.gapCells))
|
|
y: track.overhang
|
|
width: root.spanWidth(root.layout.gapCells)
|
|
height: CcMetrics.footerHeight
|
|
radius: Theme.fullRadius(width, height)
|
|
color: Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
|
|
visible: root.layout.gapOffset >= 0 && root.layout.gapCells > 0
|
|
}
|
|
|
|
Repeater {
|
|
model: ScriptModel {
|
|
values: root.keys
|
|
}
|
|
|
|
Item {
|
|
id: footerItem
|
|
|
|
required property int index
|
|
readonly property var entry: root.items[index] ?? ({
|
|
"index": -1,
|
|
"widget": ({})
|
|
})
|
|
readonly property int cells: root.cells[index] ?? 0
|
|
readonly property bool lifted: root.liftedIndex === entry.index
|
|
readonly property real span: root.spanWidth(Math.max(1, cells)) + (index === root.slackIndex ? root.fillSlack : 0)
|
|
readonly property real restX: root.xFor(root.layout.offsets[index] ?? 0, span)
|
|
property point grab: Qt.point(0, 0)
|
|
property point dragScene: Qt.point(0, 0)
|
|
readonly property point dragPosition: {
|
|
// The panel can grow under a drag and move the row, so the pointer is re-mapped whenever it does.
|
|
root.y;
|
|
const point = track.mapFromItem(null, dragScene.x, dragScene.y);
|
|
return Qt.point(point.x - grab.x, point.y - grab.y);
|
|
}
|
|
property bool landed: false
|
|
readonly property bool flying: lifted || landed
|
|
readonly property bool atEnd: root.ends[index] ?? false
|
|
|
|
x: lifted || landed ? dragPosition.x : restX
|
|
y: lifted || landed ? dragPosition.y : track.overhang
|
|
z: lifted || landed ? 1 : 0
|
|
width: span
|
|
height: CcMetrics.footerHeight
|
|
visible: cells > 0
|
|
|
|
// A reorder or a switch of side is the moment a dropped item leaves the pointer for its slot.
|
|
onIndexChanged: landed = false
|
|
onAtEndChanged: landed = false
|
|
onFlyingChanged: {
|
|
if (flying) {
|
|
root.flyingItem = footerItem;
|
|
return;
|
|
}
|
|
if (root.flyingItem === footerItem)
|
|
root.flyingItem = null;
|
|
}
|
|
|
|
Behavior on x {
|
|
enabled: !footerItem.lifted && !footerItem.landed && CcMetrics.animationsEnabled
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.expressiveEffects
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
|
}
|
|
}
|
|
|
|
CcTileLoader {
|
|
id: tile
|
|
|
|
anchors.fill: parent
|
|
grid: root.grid
|
|
widgetData: footerItem.entry.widget
|
|
savedIndex: footerItem.entry.index
|
|
columns: Math.max(1, footerItem.cells)
|
|
rows: 1
|
|
compact: footerItem.cells <= 1
|
|
small: true
|
|
docked: true
|
|
}
|
|
|
|
CcEditChrome {
|
|
id: chrome
|
|
|
|
property real startX: 0
|
|
property int startCells: 0
|
|
property bool startFill: false
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: -contentInset
|
|
z: 2
|
|
visible: root.editMode
|
|
widgetData: footerItem.entry.widget
|
|
passthrough: tile.item?.passthrough ?? null
|
|
dragging: footerItem.lifted
|
|
resizing: root.resizePreview?.index === footerItem.entry.index
|
|
horizontalResize: true
|
|
sizeText: root.resizePreview?.index === footerItem.entry.index && root.resizePreview.fill ? I18n.tr("Fill") : String(footerItem.cells)
|
|
cornerRadius: tile.item?.bodyRadius ?? Theme.fullRadius(footerItem.width, footerItem.height)
|
|
onResizeStarted: (px, py) => {
|
|
startX = mapToItem(null, px, py).x;
|
|
startCells = footerItem.cells;
|
|
startFill = root.fills[footerItem.index] ?? false;
|
|
root.resizePreview = {
|
|
"index": footerItem.entry.index,
|
|
"cells": startCells,
|
|
"fill": startFill
|
|
};
|
|
}
|
|
// Pulling past the edit buttons turns the item into a fill, which runs on to the pencil outside edit mode.
|
|
onResizeMoved: (px, py) => {
|
|
const delta = (mapToItem(null, px, py).x - startX) * (I18n.isRtl ? -1 : 1) / root.trackScale;
|
|
const min = root.mins[footerItem.index];
|
|
const max = Math.max(min, root.capacity - (root.fixedUsed - root.fixedCells[footerItem.index]));
|
|
const wanted = Math.round(startCells + delta / root.pitch);
|
|
root.resizePreview = {
|
|
"index": footerItem.entry.index,
|
|
"cells": Math.max(min, Math.min(max, wanted)),
|
|
"fill": startFill ? wanted >= max : wanted > max
|
|
};
|
|
}
|
|
onResizeEnded: {
|
|
const preview = root.resizePreview;
|
|
const cells = preview?.cells ?? startCells;
|
|
const fill = preview?.fill ?? startFill;
|
|
if (cells !== startCells || fill !== startFill)
|
|
root.resized(footerItem.entry.index, cells, fill);
|
|
root.resizePreview = null;
|
|
}
|
|
onResizeCanceled: root.resizePreview = null
|
|
onRemoveRequested: root.removeRequested(footerItem.entry.index)
|
|
onConfigRequested: anchor => root.configRequested(footerItem.entry.index, footerItem.entry.widget, anchor)
|
|
}
|
|
|
|
DragHandler {
|
|
id: dragHandler
|
|
|
|
target: null
|
|
enabled: root.editMode && root.resizePreview === null && !footerItem.landed
|
|
cursorShape: Qt.ClosedHandCursor
|
|
onActiveChanged: {
|
|
if (active) {
|
|
footerItem.grab = footerItem.mapFromItem(null, centroid.scenePressPosition.x, centroid.scenePressPosition.y);
|
|
footerItem.dragScene = centroid.scenePosition;
|
|
root.liftedPoint = centroid.scenePosition;
|
|
root.liftedIndex = footerItem.entry.index;
|
|
root.liftedSlot = root.insertionAt(centroid.scenePosition);
|
|
return;
|
|
}
|
|
footerItem.landed = root.dropHandler?.(footerItem.entry.index, footerItem.cells, footerItem.dragScene) ?? false;
|
|
root.liftedIndex = -1;
|
|
root.liftedSlot = null;
|
|
}
|
|
onCentroidChanged: {
|
|
if (!active)
|
|
return;
|
|
footerItem.dragScene = centroid.scenePosition;
|
|
root.liftedPoint = centroid.scenePosition;
|
|
const visual = footerItem.mapToItem(null, 0, 0, footerItem.width, footerItem.height);
|
|
const leaving = !root.overTrash && (root.leavesRow?.(visual) ?? false);
|
|
root.liftedSlot = leaving || root.overTrash ? null : root.insertionAt(footerItem.dragScene);
|
|
root.itemMoved(footerItem.entry.index, visual, leaving);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
anchors.right: trailing.left
|
|
anchors.rightMargin: CcMetrics.footerGap
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: root.spacing
|
|
visible: root.editMode && !root.dragging
|
|
|
|
DankActionButton {
|
|
buttonSize: Theme.iconButtonSize
|
|
iconName: "add"
|
|
iconSize: CcMetrics.iconBoxIconSize
|
|
iconColor: Theme.onSecondaryContainer
|
|
backgroundColor: Theme.secondaryContainer
|
|
tooltipText: I18n.tr("Add widget")
|
|
onClicked: root.addWidgetRequested()
|
|
}
|
|
|
|
DankActionButton {
|
|
id: moreButton
|
|
|
|
buttonSize: Theme.iconButtonSize
|
|
iconName: "more_horiz"
|
|
iconSize: CcMetrics.iconBoxIconSize
|
|
iconColor: CcMetrics.tileInactiveContent
|
|
backgroundColor: CcMetrics.tileInactiveColor
|
|
border.width: Theme.layerOutlineWidth
|
|
border.color: Theme.outlineMedium
|
|
tooltipText: I18n.tr("More")
|
|
onClicked: editMenu.openAt(moreButton)
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
id: trash
|
|
|
|
anchors.right: trailing.left
|
|
anchors.rightMargin: CcMetrics.footerGap
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: root.editActionsWidth
|
|
height: CcMetrics.footerHeight
|
|
radius: Theme.fullRadius(width, height)
|
|
color: root.overTrash ? Theme.errorContainer : CcMetrics.tileInactiveColor
|
|
visible: root.editMode && root.dragging
|
|
Accessible.role: Accessible.Graphic
|
|
Accessible.name: I18n.tr("Remove")
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Theme.standardEasing
|
|
}
|
|
}
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "delete"
|
|
size: CcMetrics.iconBoxIconSize
|
|
filled: root.overTrash
|
|
color: root.overTrash ? Theme.onErrorContainer : CcMetrics.tileInactiveContent
|
|
}
|
|
}
|
|
|
|
CcMenu {
|
|
id: editMenu
|
|
|
|
transientSurfaceTracker: root.transientSurfaceTracker
|
|
items: [
|
|
{
|
|
"iconName": root.onTop ? "vertical_align_bottom" : "vertical_align_top",
|
|
"label": root.onTop ? I18n.tr("Move down") : I18n.tr("Move up"),
|
|
"action": () => root.moveRequested()
|
|
},
|
|
{
|
|
"iconName": "settings_backup_restore",
|
|
"label": I18n.tr("Reset to default"),
|
|
"action": () => root.resetRequested()
|
|
},
|
|
{
|
|
"iconName": "clear_all",
|
|
"label": I18n.tr("Clear All"),
|
|
"destructive": true,
|
|
"action": () => root.clearRequested()
|
|
},
|
|
{
|
|
"iconName": "close",
|
|
"label": I18n.tr("Discard"),
|
|
"action": () => root.cancelRequested()
|
|
}
|
|
]
|
|
}
|
|
|
|
Item {
|
|
id: trailing
|
|
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
// One column, the same pill as a small button docked beside it.
|
|
width: root.spanWidth(1)
|
|
height: CcMetrics.footerHeight
|
|
|
|
DankActionButton {
|
|
anchors.fill: parent
|
|
buttonSize: CcMetrics.footerHeight
|
|
iconName: "check"
|
|
iconSize: CcMetrics.iconBoxIconSize
|
|
iconColor: Theme.onPrimary
|
|
backgroundColor: Theme.primary
|
|
tooltipText: I18n.tr("Save")
|
|
visible: root.editMode
|
|
onClicked: root.editToggled()
|
|
}
|
|
|
|
DankActionButton {
|
|
anchors.fill: parent
|
|
buttonSize: CcMetrics.footerHeight
|
|
iconName: "edit"
|
|
iconSize: CcMetrics.iconBoxIconSize
|
|
iconColor: CcMetrics.tileInactiveContent
|
|
backgroundColor: CcMetrics.tileInactiveColor
|
|
border.width: Theme.layerOutlineWidth
|
|
border.color: Theme.outlineMedium
|
|
tooltipText: I18n.tr("Edit")
|
|
visible: !root.editMode
|
|
onClicked: root.editToggled()
|
|
}
|
|
}
|
|
|
|
// The track's layer only draws inside the row, so a lifted item is shown from here while it travels.
|
|
ShaderEffectSource {
|
|
readonly property real pad: PopoutMetrics.editOverflow
|
|
readonly property point at: {
|
|
const item = root.flyingItem;
|
|
if (!item)
|
|
return Qt.point(0, 0);
|
|
item.x;
|
|
item.y;
|
|
trackScale.xScale;
|
|
return root.mapFromItem(item, -pad, -pad);
|
|
}
|
|
|
|
visible: root.flyingItem !== null
|
|
sourceItem: root.flyingItem
|
|
hideSource: true
|
|
smooth: true
|
|
mipmap: true
|
|
sourceRect: root.flyingItem ? Qt.rect(-pad, -pad, root.flyingItem.width + pad * 2, root.flyingItem.height + pad * 2) : Qt.rect(0, 0, 0, 0)
|
|
x: at.x
|
|
y: at.y
|
|
width: sourceRect.width * trackScale.xScale
|
|
height: sourceRect.height * trackScale.yScale
|
|
}
|
|
}
|