horizontal media widget
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
default property alias content: contentColumn.data
|
||||
property string description: ""
|
||||
property bool expanded: false
|
||||
property bool nested: false
|
||||
property bool showBackground: false
|
||||
required property string title
|
||||
|
||||
signal toggleRequested
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Item {
|
||||
id: sectionHeaderItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48)
|
||||
|
||||
RowLayout {
|
||||
id: titleRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomText {
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
text: root.title
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
rotation: root.expanded ? 180 : 0
|
||||
text: "expand_more"
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.toggleRequested();
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
radius: Appearance.rounding.normal
|
||||
showHoverBackground: false
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0
|
||||
clip: true
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: backgroundRect
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.transparency.enabled ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? DynamicColors.palette.m3surfaceContainerHigh : DynamicColors.palette.m3surfaceContainer)
|
||||
opacity: root.showBackground && root.expanded ? 1.0 : 0.0
|
||||
radius: Appearance.rounding.normal
|
||||
visible: root.showBackground
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
|
||||
anchors.bottomMargin: Appearance.spacing.small
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
opacity: root.expanded ? 1.0 : 0.0
|
||||
spacing: Appearance.spacing.small
|
||||
y: Appearance.spacing.small
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: descriptionText
|
||||
|
||||
Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
text: root.description
|
||||
visible: root.description !== ""
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property string displayText: root.value.toString()
|
||||
property bool isEditing: false
|
||||
property real max: Infinity
|
||||
property real min: -Infinity
|
||||
property alias repeatRate: timer.interval
|
||||
property real step: 1
|
||||
property real value
|
||||
|
||||
signal valueModified(value: real)
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
onValueChanged: {
|
||||
if (!root.isEditing) {
|
||||
root.displayText = root.value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
id: textField
|
||||
|
||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||
leftPadding: Appearance.padding.normal
|
||||
padding: Appearance.padding.small
|
||||
rightPadding: Appearance.padding.normal
|
||||
text: root.isEditing ? text : root.displayText
|
||||
|
||||
background: CustomRect {
|
||||
color: DynamicColors.tPalette.m3surfaceContainerHigh
|
||||
implicitWidth: 100
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
validator: DoubleValidator {
|
||||
bottom: root.min
|
||||
decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0
|
||||
top: root.max
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
const numValue = parseFloat(text);
|
||||
if (!isNaN(numValue)) {
|
||||
const clampedValue = Math.max(root.min, Math.min(root.max, numValue));
|
||||
root.value = clampedValue;
|
||||
root.displayText = clampedValue.toString();
|
||||
root.valueModified(clampedValue);
|
||||
} else {
|
||||
text = root.displayText;
|
||||
}
|
||||
root.isEditing = false;
|
||||
}
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
root.isEditing = true;
|
||||
} else {
|
||||
root.isEditing = false;
|
||||
root.displayText = root.value.toString();
|
||||
}
|
||||
}
|
||||
onEditingFinished: {
|
||||
if (text !== root.displayText) {
|
||||
const numValue = parseFloat(text);
|
||||
if (!isNaN(numValue)) {
|
||||
const clampedValue = Math.max(root.min, Math.min(root.max, numValue));
|
||||
root.value = clampedValue;
|
||||
root.displayText = clampedValue.toString();
|
||||
root.valueModified(clampedValue);
|
||||
} else {
|
||||
text = root.displayText;
|
||||
}
|
||||
}
|
||||
root.isEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
color: DynamicColors.palette.m3primary
|
||||
implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitWidth: implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
StateLayer {
|
||||
id: upState
|
||||
|
||||
function onClicked(): void {
|
||||
let newValue = Math.min(root.max, root.value + root.step);
|
||||
// Round to avoid floating point precision errors
|
||||
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
|
||||
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
root.value = newValue;
|
||||
root.displayText = newValue.toString();
|
||||
root.valueModified(newValue);
|
||||
}
|
||||
|
||||
color: DynamicColors.palette.m3onPrimary
|
||||
|
||||
onPressAndHold: timer.start()
|
||||
onReleased: timer.stop()
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: upIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: DynamicColors.palette.m3onPrimary
|
||||
text: "keyboard_arrow_up"
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
color: DynamicColors.palette.m3primary
|
||||
implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitWidth: implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
StateLayer {
|
||||
id: downState
|
||||
|
||||
function onClicked(): void {
|
||||
let newValue = Math.max(root.min, root.value - root.step);
|
||||
// Round to avoid floating point precision errors
|
||||
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
|
||||
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
root.value = newValue;
|
||||
root.displayText = newValue.toString();
|
||||
root.valueModified(newValue);
|
||||
}
|
||||
|
||||
color: DynamicColors.palette.m3onPrimary
|
||||
|
||||
onPressAndHold: timer.start()
|
||||
onReleased: timer.stop()
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: downIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: DynamicColors.palette.m3onPrimary
|
||||
text: "keyboard_arrow_down"
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timer
|
||||
|
||||
interval: 100
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
if (upState.pressed)
|
||||
upState.onClicked();
|
||||
else if (downState.pressed)
|
||||
downState.onClicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: DynamicColors.palette.m3onSurface
|
||||
property int fadeStrengthAnimMs: 180
|
||||
property real fadeStrengthIdle: 0.0
|
||||
property real fadeStrengthMoving: 1.0
|
||||
property alias font: elideText.font
|
||||
property int gap: 40
|
||||
property alias horizontalAlignment: elideText.horizontalAlignment
|
||||
property bool leftFadeEnabled: false
|
||||
property real leftFadeStrength: overflowing && leftFadeEnabled ? fadeStrengthMoving : fadeStrengthIdle
|
||||
property int leftFadeWidth: 28
|
||||
property bool marqueeEnabled: true
|
||||
readonly property bool overflowing: metrics.width > root.width
|
||||
property int pauseMs: 1200
|
||||
property real pixelsPerSecond: 40
|
||||
property real rightFadeStrength: overflowing ? fadeStrengthMoving : fadeStrengthIdle
|
||||
property int rightFadeWidth: 28
|
||||
property bool sliding: false
|
||||
property alias text: elideText.text
|
||||
|
||||
function durationForDistance(px): int {
|
||||
return Math.max(1, Math.round(Math.abs(px) / root.pixelsPerSecond * 1000));
|
||||
}
|
||||
|
||||
clip: true
|
||||
implicitHeight: elideText.implicitHeight
|
||||
|
||||
Behavior on leftFadeStrength {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on rightFadeStrength {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: strip.x = 0
|
||||
onVisibleChanged: if (!visible)
|
||||
strip.x = 0
|
||||
onWidthChanged: strip.x = 0
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
font: elideText.font
|
||||
text: elideText.text
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: elideText
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.color
|
||||
elide: Text.ElideRight
|
||||
visible: !root.overflowing
|
||||
width: root.width
|
||||
}
|
||||
|
||||
Item {
|
||||
id: marqueeViewport
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
layer.enabled: true
|
||||
visible: root.overflowing
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: rightFadeMask
|
||||
}
|
||||
|
||||
Item {
|
||||
id: strip
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: t1.implicitHeight
|
||||
width: t1.width + root.gap + t2.width
|
||||
x: 0
|
||||
|
||||
CustomText {
|
||||
id: t1
|
||||
|
||||
color: root.color
|
||||
text: elideText.text
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: t2
|
||||
|
||||
color: root.color
|
||||
text: t1.text
|
||||
x: t1.width + root.gap
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: marqueeAnim
|
||||
|
||||
loops: Animation.Infinite
|
||||
running: root.marqueeEnabled && root.overflowing && root.visible
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
strip.x = 0;
|
||||
root.sliding = false;
|
||||
root.leftFadeEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: root.pauseMs
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
root.sliding = true;
|
||||
root.leftFadeEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: root.durationForDistance(t1.width)
|
||||
easing.bezierCurve: Easing.Linear
|
||||
easing.type: Easing.Linear
|
||||
from: 0
|
||||
property: "x"
|
||||
target: strip
|
||||
to: -t1.width
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
root.leftFadeEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
duration: root.durationForDistance(root.gap)
|
||||
easing.bezierCurve: Easing.Linear
|
||||
easing.type: Easing.Linear
|
||||
from: -t1.width
|
||||
property: "x"
|
||||
target: strip
|
||||
to: -(t1.width + root.gap)
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
root.sliding = false;
|
||||
strip.x = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rightFadeMask
|
||||
|
||||
readonly property real fadeStartPos: {
|
||||
const w = Math.max(1, width);
|
||||
return Math.max(0, Math.min(1, (w - root.rightFadeWidth) / w));
|
||||
}
|
||||
readonly property real leftFadeEndPos: {
|
||||
const w = Math.max(1, width);
|
||||
return Math.max(0, Math.min(1, root.leftFadeWidth / w));
|
||||
}
|
||||
|
||||
anchors.fill: marqueeViewport
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0 - root.leftFadeStrength)
|
||||
position: 0.0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0)
|
||||
position: rightFadeMask.leftFadeEndPos
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0)
|
||||
position: rightFadeMask.fadeStartPos
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(1, 1, 1, 1.0 - root.rightFadeStrength)
|
||||
position: 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
|
||||
Elevation {
|
||||
id: root
|
||||
|
||||
property MenuItem active: items[0] ?? null
|
||||
property bool expanded
|
||||
property list<MenuItem> items
|
||||
|
||||
signal itemSelected(item: MenuItem)
|
||||
|
||||
implicitHeight: root.expanded ? column.implicitHeight : 0
|
||||
implicitWidth: Math.max(200, column.implicitWidth)
|
||||
level: 2
|
||||
opacity: root.expanded ? 1 : 0
|
||||
radius: Appearance.rounding.small / 2
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3surfaceContainer
|
||||
radius: parent.radius
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: root.items
|
||||
|
||||
CustomRect {
|
||||
id: item
|
||||
|
||||
readonly property bool active: modelData === root.active
|
||||
required property int index
|
||||
required property MenuItem modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: Qt.alpha(DynamicColors.palette.m3secondaryContainer, active ? 1 : 0)
|
||||
implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2
|
||||
implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.itemSelected(item.modelData);
|
||||
root.active = item.modelData;
|
||||
root.expanded = false;
|
||||
}
|
||||
|
||||
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
|
||||
disabled: !root.expanded
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: menuOptionRow
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurfaceVariant
|
||||
text: item.modelData.icon
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
|
||||
text: item.modelData.text
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
active: item.modelData.trailingIcon.length > 0
|
||||
visible: active
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
|
||||
text: item.modelData.trailingIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
property string activeIcon: icon
|
||||
property string activeText: text
|
||||
property string icon
|
||||
required property string text
|
||||
property string trailingIcon
|
||||
property var value
|
||||
|
||||
signal clicked
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property string label
|
||||
required property real max
|
||||
required property real min
|
||||
property var onValueModified: function (value) {}
|
||||
property real step: 1
|
||||
required property real value
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2)
|
||||
implicitHeight: row.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: root.label
|
||||
}
|
||||
|
||||
CustomSpinBox {
|
||||
max: root.max
|
||||
min: root.min
|
||||
step: root.step
|
||||
value: root.value
|
||||
|
||||
onValueModified: value => {
|
||||
root.onValueModified(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user