Files

420 lines
17 KiB
QML

import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.DCommon.Widgets
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var widgetData: ({})
property var widgetDef: null
property var host: null
property bool live: true
property string iconName: ""
property string title: ""
property string subtitle: ""
property bool active: false
property real columns: 4
property real rows: 1
property bool compact: columns <= 2 && rows === 1
property bool small: false
// Set by a background-less Quick tiles group.
property bool bare: false
property real quickContentWidth: 0
readonly property real quickWidth: Math.max(bare ? CcMetrics.quickBareWidth : CcMetrics.iconBoxSize, quickContentWidth + (bare ? CcMetrics.quickBarePadding : Theme.spacingM * 2))
property real bodyInset: small ? Math.max(0, (height - CcMetrics.iconBoxSize) / 2) : 0
readonly property real bodyHeight: height - bodyInset * 2
property bool toggle: !opensPage
property Component expandedContent: null
// Replaces the icon and labels inside the body; the chrome, focus and activation stay.
property Component bodyContent: null
property Component iconContent: null
property real expandedMinimumHeight: Theme.listItemHeight
readonly property Item expandedItem: expandedLoader.item
readonly property bool expanded: expandedContent !== null && width >= CcMetrics.expandedTileMinWidth && height >= headerHeight + expandedMinimumHeight + tilePadding * 2 + Theme.spacingM
readonly property real tilePadding: tall && !narrow ? Theme.spacingM : Theme.spacingS
readonly property real baseIconExtent: Math.min(Math.max(Theme.minimumTouchTargetSize, CcMetrics.iconBoxSize), bodyHeight - tilePadding * 2, width - tilePadding * 2)
readonly property real iconExtent: stacked ? Math.max(0, Math.min(baseIconExtent, height - tilePadding * 2 - Theme.spacingS - titleLabel.implicitHeight)) : baseIconExtent
readonly property real headerHeight: Math.max(baseIconExtent, Theme.fontSizeLarge + Theme.fontSizeMedium + Theme.spacingS)
readonly property bool stacked: tall && !expanded && width <= height
readonly property bool narrow: width < CcMetrics.expandedTileMinWidth
readonly property bool denseText: narrow || small
readonly property real labelHeight: titleLabel.implicitHeight + (showSubtitle ? Theme.spacingXXS + subtitleLabel.implicitHeight : 0)
readonly property real textGap: CcMetrics.tileTextGap
readonly property bool showSubtitle: subtitle !== "" && (!stacked || height - tilePadding * 2 >= iconExtent + Theme.spacingS + titleLabel.implicitHeight + Theme.spacingXXS + subtitleLabel.implicitHeight)
property bool showExpand: false
property bool opensPage: false
property color restIconColor: CcMetrics.tileInactiveIcon
property Component tallContent: null
// The tile's own state. The island disables a face while it fades in, and that must not restyle the tile.
property bool available: true
property bool interactive: true
property bool iconBlinking: false
property bool tooltips: true
property real iconRotation: 0
signal clicked
signal expandClicked
signal wheel(var wheelEvent)
readonly property bool tall: height >= CcMetrics.gridRowUnit * 2
readonly property bool hasIconBox: (showExpand || opensPage || expanded) && !compact
readonly property bool showsActive: active && interactive
readonly property real restRadius: {
if (showsActive)
return Math.min(CcMetrics.tileActiveRadius, width / 2, bodyHeight / 2);
return tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : Theme.fullRadius(width, bodyHeight);
}
property bool acceptsInput: interactive && enabled
readonly property bool bodyActive: showsActive && !hasIconBox && !bare
readonly property color bodyColor: {
if (bare)
return "transparent";
if (!available)
return Theme.onSurface_12;
return bodyActive ? CcMetrics.tileActiveColor : CcMetrics.tileInactiveColor;
}
readonly property color contentColor: {
if (!available)
return Theme.onSurface_38;
return bodyActive ? CcMetrics.tileActiveContent : CcMetrics.tileInactiveContent;
}
readonly property color subtitleColor: {
if (!available)
return Theme.onSurface_38;
return bodyActive ? CcMetrics.tileActiveContent : CcMetrics.tileInactiveSubtitle;
}
readonly property color iconColor: {
if (!available)
return Theme.onSurface_38;
if (hasIconBox)
return showsActive ? CcMetrics.tileActiveContent : CcMetrics.tileInactiveContent;
// Without a body, toggles show their state in the icon tint alone.
if (bare && showsActive)
return CcMetrics.bareActiveIcon;
if (bare && toggle)
return CcMetrics.tileInactiveContent;
return bodyActive ? CcMetrics.tileActiveContent : root.restIconColor;
}
readonly property color iconBoxColor: {
if (!available)
return Theme.onSurface_12;
return showsActive ? CcMetrics.tileActiveColor : CcMetrics.iconBoxInactiveColor;
}
property real bodyRadius: bodyLayer.pressed ? Math.min(Theme.cornerRadiusM, width / 2, bodyHeight / 2) : restRadius
property real iconBoxRadius: {
if (boxLayer.pressed)
return Math.min(Theme.cornerRadiusS, CcMetrics.iconBoxSize / 2);
if (showsActive)
return Math.min(CcMetrics.iconBoxActiveRadius, CcMetrics.iconBoxSize / 2);
return Theme.fullRadius(CcMetrics.iconBoxSize, CcMetrics.iconBoxSize);
}
width: parent?.width ?? 0
height: CcMetrics.tileHeight
enabled: available
activeFocusOnTab: acceptsInput
Accessible.role: toggle && (!showExpand || compact) && !opensPage ? Accessible.CheckBox : Accessible.Button
Accessible.checkable: toggle && (!showExpand || compact) && !opensPage
Accessible.checked: active
Accessible.name: title
Accessible.description: subtitle
Accessible.onPressAction: activate()
Accessible.onToggleAction: {
if (toggle && (!showExpand || compact) && !opensPage)
activate();
}
function activate() {
if (!acceptsInput)
return;
if (showExpand && !compact && !opensPage) {
expandClicked();
return;
}
clicked();
}
function expand() {
if (!showExpand && !opensPage)
return;
expandClicked();
}
Keys.onPressed: event => {
if (!acceptsInput)
return;
switch (event.key) {
case Qt.Key_Space:
case Qt.Key_Return:
case Qt.Key_Enter:
root.activate();
event.accepted = true;
break;
}
}
// Animated so a half-row resize reads as the body shrinking.
Behavior on bodyInset {
enabled: CcMetrics.animationsEnabled
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Behavior on bodyRadius {
enabled: CcMetrics.animationsEnabled && !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
Behavior on iconBoxRadius {
enabled: CcMetrics.animationsEnabled && !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
Rectangle {
id: body
anchors.fill: parent
anchors.topMargin: root.bodyInset
anchors.bottomMargin: root.bodyInset
radius: root.bodyRadius
color: root.bodyColor
border.width: root.bare ? 0 : Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Behavior on color {
enabled: CcMetrics.animationsEnabled && !SettingsData.reduceMotion
ColorAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
StateLayer {
id: bodyLayer
enabled: root.acceptsInput
disabled: !root.acceptsInput
anchors.bottomMargin: root.expanded ? root.height - root.headerHeight - root.tilePadding * 2 : 0
stateColor: root.contentColor
cornerRadius: root.bodyRadius
tooltipText: root.tooltips && root.compact ? [root.title, root.subtitle].filter(text => text !== "").join(" · ") : ""
onClicked: root.activate()
onPressAndHold: root.expand()
onWheel: wheelEvent => {
wheelEvent.accepted = false;
root.wheel(wheelEvent);
}
}
FocusRing {
radius: root.bodyRadius + Theme.focusRingOffset
visible: root.activeFocus
}
DIcon {
id: compactIcon
anchors.centerIn: parent
name: root.iconName
size: root.small ? CcMetrics.iconBoxIconSize : CcMetrics.tileIconSize
color: root.iconColor
filled: root.showsActive
rotation: root.iconRotation
visible: root.compact && root.bodyContent === null && root.iconContent === null
DBlink {
target: compactIcon
running: root.iconBlinking && root.compact && root.visible && root.live
}
}
Loader {
anchors.centerIn: parent
active: root.compact && root.bodyContent === null && root.iconContent !== null
sourceComponent: root.iconContent
}
Loader {
id: bodyLoader
anchors.fill: parent
active: root.bodyContent !== null
sourceComponent: root.bodyContent
}
Item {
id: content
anchors.fill: parent
anchors.margins: root.tilePadding
visible: !root.compact && root.bodyContent === null
Rectangle {
id: iconBox
objectName: "tileIconBox"
x: root.stacked && root.narrow ? (parent.width - width) / 2 : root.LayoutMirroring.enabled ? parent.width - width : 0
y: root.stacked ? Math.max(0, (parent.height - height - Theme.spacingS - root.labelHeight) / 2) : root.expanded ? 0 : (parent.height - height) / 2
width: root.iconExtent
height: width
radius: root.hasIconBox ? root.iconBoxRadius : 0
color: root.hasIconBox ? root.iconBoxColor : "transparent"
border.width: root.hasIconBox ? Theme.layerOutlineWidth : 0
border.color: Theme.outlineMedium
activeFocusOnTab: root.showExpand && root.toggle && root.acceptsInput
Accessible.role: root.toggle ? Accessible.CheckBox : Accessible.Button
Accessible.name: root.title
Accessible.description: root.subtitle
Accessible.checkable: root.toggle
Accessible.checked: root.active
Accessible.ignored: !root.showExpand || !root.toggle
Accessible.onPressAction: activate()
Accessible.onToggleAction: activate()
function activate() {
if (root.acceptsInput && root.toggle)
root.clicked();
}
Keys.onPressed: event => {
if (!root.acceptsInput || !root.toggle)
return;
switch (event.key) {
case Qt.Key_Space:
case Qt.Key_Return:
case Qt.Key_Enter:
activate();
event.accepted = true;
break;
}
}
DIcon {
id: tileIcon
anchors.centerIn: parent
name: root.iconName
size: root.hasIconBox ? CcMetrics.iconBoxIconSize : CcMetrics.tileIconSize
color: root.iconColor
filled: root.showsActive
rotation: root.iconRotation
visible: root.iconContent === null
DBlink {
target: tileIcon
running: root.iconBlinking && !root.compact && root.visible && root.live
}
}
Loader {
anchors.centerIn: parent
active: root.iconContent !== null && !root.compact
sourceComponent: root.iconContent
}
StateLayer {
id: boxLayer
visible: root.showExpand && root.toggle
enabled: visible && root.acceptsInput
disabled: !root.acceptsInput
stateColor: root.iconColor
cornerRadius: root.iconBoxRadius
onClicked: iconBox.activate()
}
FocusRing {
radius: root.iconBoxRadius + Theme.focusRingOffset
visible: iconBox.activeFocus
}
}
Loader {
id: meterLoader
anchors.left: iconBox.right
anchors.leftMargin: root.textGap
anchors.right: parent.right
y: iconBox.y
height: iconBox.height
active: root.stacked && !root.narrow && root.tallContent !== null
sourceComponent: root.tallContent
}
Item {
id: labels
objectName: "tileLabels"
x: root.stacked || root.LayoutMirroring.enabled ? 0 : root.baseIconExtent + root.textGap
width: root.stacked ? parent.width : Math.max(0, parent.width - root.baseIconExtent - root.textGap)
y: root.stacked ? iconBox.y + iconBox.height + Theme.spacingS : root.expanded ? (root.headerHeight - height) / 2 : (parent.height - height) / 2
height: root.labelHeight
StyledText {
id: titleLabel
objectName: "tileTitle"
width: parent.width
text: root.title
color: root.contentColor
font.pixelSize: root.denseText ? Theme.fontSizeMedium : Theme.fontSizeLarge
font.weight: Theme.fontWeightMedium
elide: Text.ElideRight
wrapMode: root.stacked ? Text.Wrap : Text.NoWrap
maximumLineCount: root.stacked ? 2 : 1
horizontalAlignment: root.stacked && root.narrow ? Text.AlignHCenter : Text.AlignLeft
}
StyledText {
id: subtitleLabel
objectName: "tileSubtitle"
y: titleLabel.implicitHeight + Theme.spacingXXS
width: parent.width
text: root.subtitle
color: root.subtitleColor
font.pixelSize: root.denseText ? Theme.fontSizeSmall : Theme.fontSizeMedium
elide: Text.ElideRight
wrapMode: Text.NoWrap
horizontalAlignment: root.stacked && root.narrow ? Text.AlignHCenter : Text.AlignLeft
visible: root.showSubtitle
}
}
Loader {
id: expandedLoader
objectName: "tileExpandedContent"
anchors.left: parent.left
anchors.right: parent.right
y: root.headerHeight + Theme.spacingM
height: Math.max(0, parent.height - y)
active: root.expanded && root.live
visible: root.expanded
enabled: root.acceptsInput
sourceComponent: root.expandedContent
}
}
}
Binding {
target: expandedLoader.item
property: "tile"
value: root
when: expandedLoader.item !== null && "tile" in expandedLoader.item
}
Binding {
target: bodyLoader.item
property: "tile"
value: root
when: bodyLoader.item !== null && "tile" in bodyLoader.item
}
}