mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-16 00:04:01 -04:00
420 lines
17 KiB
QML
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
|
|
}
|
|
}
|