add volume and brightness control
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
// The sun, drawn: a disc and eight rays. Same reason as the other glyphs —
|
||||
// symbolic icons come out black on the dark panel, and a drawn one recolours.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: Theme.text
|
||||
property int size: Theme.iconSize
|
||||
|
||||
implicitWidth: root.size
|
||||
implicitHeight: root.size
|
||||
|
||||
readonly property real stroke: Math.max(1.5, Math.round(root.size * 0.09))
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: Math.round(root.size * 0.42)
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: root.color
|
||||
}
|
||||
|
||||
// One ray, drawn at twelve o'clock and rotated about the centre of the
|
||||
// glyph into each of the eight positions.
|
||||
Repeater {
|
||||
model: 8
|
||||
|
||||
Rectangle {
|
||||
id: ray
|
||||
|
||||
required property int index
|
||||
|
||||
readonly property real inset: root.size * 0.04
|
||||
|
||||
x: (root.size - width) / 2
|
||||
y: ray.inset
|
||||
width: root.stroke
|
||||
height: Math.round(root.size * 0.16)
|
||||
radius: width / 2
|
||||
color: root.color
|
||||
|
||||
transform: Rotation {
|
||||
origin.x: ray.width / 2
|
||||
origin.y: root.size / 2 - ray.inset
|
||||
angle: ray.index * 45
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
// A themed 0-1 slider. Reports intent through `moved` rather than driving
|
||||
// `value` itself, the same way ToggleSwitch does, so the caller can keep it
|
||||
// bound to the real state — the volume pipewire ended up at, the brightness the
|
||||
// backlight accepted — and let the slider follow.
|
||||
//
|
||||
// While a drag is in flight the knob tracks the pointer instead, so a backend
|
||||
// that answers a moment late cannot make it stutter under the finger.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real value: 0
|
||||
// One wheel notch, one arrow key.
|
||||
property real stepSize: 0.05
|
||||
property color fillColor: Theme.accent
|
||||
|
||||
signal moved(real value)
|
||||
|
||||
// Where the drag has got to, used in place of `value` until it ends.
|
||||
property real dragValue: 0
|
||||
|
||||
readonly property real position: mouse.pressed ? root.dragValue : Math.max(0, Math.min(1, root.value))
|
||||
|
||||
// The hit area is a good deal taller than the groove: this panel gets used
|
||||
// with a finger as often as a pointer.
|
||||
implicitWidth: 120
|
||||
implicitHeight: Theme.sliderKnob + Theme.padding * 2
|
||||
|
||||
readonly property real travel: root.width - Theme.sliderKnob
|
||||
|
||||
function setFromX(x: real): void {
|
||||
const position = root.travel > 0 ? (x - Theme.sliderKnob / 2) / root.travel : 0;
|
||||
const value = Math.max(0, Math.min(1, position));
|
||||
|
||||
root.dragValue = value;
|
||||
root.moved(value);
|
||||
}
|
||||
|
||||
function step(delta: real): void {
|
||||
root.moved(Math.max(0, Math.min(1, root.position + delta)));
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: groove
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
height: Theme.sliderTrack
|
||||
radius: height / 2
|
||||
color: Theme.background
|
||||
border.width: Theme.borderWidth
|
||||
border.color: Theme.border
|
||||
|
||||
Rectangle {
|
||||
// Stops under the middle of the knob, so the fill and the knob move
|
||||
// as one piece rather than the fill running out ahead of it.
|
||||
width: Math.min(groove.width, Theme.sliderKnob / 2 + root.travel * root.position)
|
||||
height: groove.height
|
||||
radius: groove.radius
|
||||
color: root.enabled ? root.fillColor : Theme.textDim
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: knob
|
||||
|
||||
x: root.travel * root.position
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.sliderKnob
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: root.enabled ? Theme.text : Theme.textDim
|
||||
border.width: Theme.borderWidth
|
||||
border.color: Theme.background
|
||||
scale: mouse.pressed ? 1.15 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDuration
|
||||
easing.type: Theme.animEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
preventStealing: true
|
||||
|
||||
// A press anywhere on the groove jumps there: on a touchscreen there is
|
||||
// no way to grab the knob first.
|
||||
onPressed: event => root.setFromX(event.x)
|
||||
onPositionChanged: event => {
|
||||
if (mouse.pressed)
|
||||
root.setFromX(event.x);
|
||||
}
|
||||
|
||||
onWheel: event => {
|
||||
const notches = event.angleDelta.y / 120;
|
||||
if (notches !== 0)
|
||||
root.step(notches * root.stepSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
// One labelled slider: glyph, name and reading on top, the slider under them.
|
||||
// Both rows of the controls panel are this.
|
||||
//
|
||||
// The glyph goes in `content`, so each row can bring its own — and, where the
|
||||
// glyph does something when clicked (mute), say so with `iconClickable`.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string valueText: ""
|
||||
property real value: 0
|
||||
property real stepSize: 0.05
|
||||
// Selected by the keyboard, which is what arrow keys move.
|
||||
property bool selected: false
|
||||
property bool iconClickable: false
|
||||
|
||||
default property alias content: glyph.data
|
||||
|
||||
signal moved(real value)
|
||||
signal iconClicked
|
||||
|
||||
implicitHeight: header.height + slider.height
|
||||
|
||||
function step(delta: real): void {
|
||||
slider.step(delta);
|
||||
}
|
||||
|
||||
Item {
|
||||
id: header
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
}
|
||||
height: Theme.iconSize
|
||||
|
||||
Item {
|
||||
id: glyph
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
}
|
||||
|
||||
// Sibling of the glyph rather than a child of it: inside, it would be
|
||||
// part of the childrenRect the glyph sizes itself from.
|
||||
MouseArea {
|
||||
anchors.fill: glyph
|
||||
enabled: root.iconClickable
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.iconClicked()
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors {
|
||||
left: glyph.right
|
||||
right: reading.left
|
||||
leftMargin: Theme.padding
|
||||
rightMargin: Theme.padding
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
text: root.label
|
||||
color: root.selected ? Theme.accent : Theme.text
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
id: reading
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
text: root.valueText
|
||||
color: Theme.textDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: slider
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: header.bottom
|
||||
}
|
||||
value: root.value
|
||||
stepSize: root.stepSize
|
||||
|
||||
onMoved: value => root.moved(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.config
|
||||
|
||||
// The speaker, drawn for the same reason as the wifi fan: themes ship it
|
||||
// symbolic-only, and drawing it lets the waves light up with the volume.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: Theme.text
|
||||
// Unlit waves stay drawn, faintly, so the glyph keeps one silhouette at
|
||||
// every volume instead of changing shape as it moves.
|
||||
property color dimColor: Theme.textDim
|
||||
property int size: Theme.iconSize
|
||||
// Lit waves, 0-2.
|
||||
property int level: 2
|
||||
// Struck through with a diagonal, for "muted".
|
||||
property bool struck: false
|
||||
|
||||
implicitWidth: root.size
|
||||
implicitHeight: root.size
|
||||
|
||||
readonly property real stroke: Math.max(1.5, root.size * 0.085)
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
// Body and cone in one outline, starting at the top left of the body.
|
||||
ShapePath {
|
||||
fillColor: root.color
|
||||
strokeWidth: -1
|
||||
|
||||
startX: root.size * 0.10
|
||||
startY: root.size * 0.38
|
||||
|
||||
PathLine {
|
||||
x: root.size * 0.28
|
||||
y: root.size * 0.38
|
||||
}
|
||||
PathLine {
|
||||
x: root.size * 0.50
|
||||
y: root.size * 0.16
|
||||
}
|
||||
PathLine {
|
||||
x: root.size * 0.50
|
||||
y: root.size * 0.84
|
||||
}
|
||||
PathLine {
|
||||
x: root.size * 0.28
|
||||
y: root.size * 0.62
|
||||
}
|
||||
PathLine {
|
||||
x: root.size * 0.10
|
||||
y: root.size * 0.62
|
||||
}
|
||||
PathLine {
|
||||
x: root.size * 0.10
|
||||
y: root.size * 0.38
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: 2
|
||||
|
||||
Shape {
|
||||
id: wave
|
||||
|
||||
required property int index
|
||||
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.level > wave.index ? root.color : root.dimColor
|
||||
strokeWidth: root.stroke
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
// Centred on the cone's mouth, opening right: -45° to 45°.
|
||||
PathAngleArc {
|
||||
centerX: root.size * 0.50
|
||||
centerY: root.size * 0.50
|
||||
radiusX: root.size * (0.18 + wave.index * 0.14)
|
||||
radiusY: root.size * (0.18 + wave.index * 0.14)
|
||||
startAngle: -45
|
||||
sweepAngle: 90
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
visible: root.struck
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.color
|
||||
strokeWidth: root.stroke
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
startX: root.size * 0.12
|
||||
startY: root.size * 0.88
|
||||
|
||||
PathLine {
|
||||
x: root.size * 0.88
|
||||
y: root.size * 0.12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user