initial commit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
// Renders a desktop-entry icon, which may be either an icon-theme name or an
|
||||
// absolute path. Falls back to a lettered tile so an entry with a broken or
|
||||
// missing icon still gets something clickable.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string fallbackLabel: ""
|
||||
// When set, shown instead of the lettered tile if the icon cannot resolve.
|
||||
property string fallbackGlyph: ""
|
||||
property color glyphColor: Theme.text
|
||||
property int size: Theme.iconSize
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
|
||||
readonly property string resolved: root.resolveIcon(root.icon)
|
||||
|
||||
function resolveIcon(name: string): string {
|
||||
if (!name)
|
||||
return "";
|
||||
if (name.startsWith("file://") || name.startsWith("image://"))
|
||||
return name;
|
||||
if (name.startsWith("/"))
|
||||
return "file://" + name;
|
||||
|
||||
// iconPath with checkExists returns "" when the theme has no such icon,
|
||||
// so these can be chained as fallbacks. The last segment of a
|
||||
// reverse-DNS app id ("org.qutebrowser.qutebrowser") is very often the
|
||||
// real icon name, so try that before giving up.
|
||||
const lower = name.toLowerCase();
|
||||
const tail = lower.split(".").pop();
|
||||
|
||||
return Quickshell.iconPath(name, true) || Quickshell.iconPath(lower, true) || (tail !== lower ? Quickshell.iconPath(tail, true) : "");
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: image
|
||||
|
||||
anchors.fill: parent
|
||||
source: root.resolved
|
||||
visible: root.resolved !== "" && image.status !== Image.Error
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !image.visible && root.fallbackGlyph !== ""
|
||||
text: root.fallbackGlyph
|
||||
color: root.glyphColor
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Math.max(10, root.size * 0.8)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !image.visible && root.fallbackGlyph === ""
|
||||
radius: Math.max(2, root.size / 5)
|
||||
color: Theme.surfaceActive
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: (root.fallbackLabel || "?").charAt(0).toUpperCase()
|
||||
color: Theme.textDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Math.max(8, root.size * 0.55)
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.config
|
||||
|
||||
// The bluetooth rune, drawn rather than themed: icon themes ship it only as a
|
||||
// symbolic (black-on-transparent) SVG, which Qt renders as-is and which would
|
||||
// be invisible on the dark panel. Drawing it also makes it recolourable to
|
||||
// signal adapter state.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: Theme.text
|
||||
property int size: Theme.iconSize
|
||||
// Struck through with a diagonal, for "off".
|
||||
property bool struck: false
|
||||
|
||||
implicitWidth: root.size
|
||||
implicitHeight: root.size
|
||||
|
||||
readonly property real glyphWidth: root.size * 0.44
|
||||
readonly property real glyphHeight: root.size * 0.82
|
||||
readonly property real stroke: Math.max(1.5, root.size * 0.09)
|
||||
|
||||
Shape {
|
||||
anchors.centerIn: parent
|
||||
width: root.glyphWidth
|
||||
height: root.glyphHeight
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
// One continuous stroke: up the left diagonal, over the top apex, down
|
||||
// the spine, out to the right and back down the other diagonal.
|
||||
ShapePath {
|
||||
strokeColor: root.color
|
||||
strokeWidth: root.stroke
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
|
||||
startX: 0
|
||||
startY: root.glyphHeight * 0.25
|
||||
|
||||
PathLine {
|
||||
x: root.glyphWidth
|
||||
y: root.glyphHeight * 0.75
|
||||
}
|
||||
PathLine {
|
||||
x: root.glyphWidth * 0.5
|
||||
y: root.glyphHeight
|
||||
}
|
||||
PathLine {
|
||||
x: root.glyphWidth * 0.5
|
||||
y: 0
|
||||
}
|
||||
PathLine {
|
||||
x: root.glyphWidth
|
||||
y: root.glyphHeight * 0.25
|
||||
}
|
||||
PathLine {
|
||||
x: 0
|
||||
y: root.glyphHeight * 0.75
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.centerIn: parent
|
||||
width: root.size
|
||||
height: root.size
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
// Shared hover/press surface used by every clickable thing on the panel.
|
||||
//
|
||||
// Content is placed at its natural size inside `holder`, which then drives the
|
||||
// button's implicit size. Content must not anchor to the holder or the size
|
||||
// binding becomes circular.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool active: false
|
||||
property int minContentWidth: 0
|
||||
property alias acceptedButtons: mouse.acceptedButtons
|
||||
property alias containsMouse: mouse.containsMouse
|
||||
property color activeColor: Theme.surfaceActive
|
||||
|
||||
default property alias content: holder.data
|
||||
|
||||
// Decorations that should stretch across the whole button (underlines,
|
||||
// badges) go here instead of `content`, so they stay out of the size
|
||||
// calculation above.
|
||||
property alias overlay: overlayHolder.data
|
||||
|
||||
signal clicked(var event)
|
||||
|
||||
implicitWidth: Math.max(root.minContentWidth, holder.childrenRect.width) + Theme.padding * 2
|
||||
implicitHeight: holder.childrenRect.height + Theme.padding
|
||||
|
||||
radius: Theme.radius
|
||||
color: root.active ? root.activeColor : mouse.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.animDuration
|
||||
easing.type: Theme.animEasing
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: holder
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
}
|
||||
|
||||
Item {
|
||||
id: overlayHolder
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: event => root.clicked(event)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
// A themed on/off switch. Reports intent through `toggled` rather than driving
|
||||
// `checked` itself, so the caller can keep it bound to the real state and let
|
||||
// the switch follow whatever actually happened.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool checked: false
|
||||
// Shown mid-flight, while the backing state is still catching up.
|
||||
property bool busy: false
|
||||
|
||||
signal toggled(bool checked)
|
||||
|
||||
implicitWidth: 38
|
||||
implicitHeight: 20
|
||||
radius: height / 2
|
||||
color: root.checked ? Theme.accent : Theme.background
|
||||
border.width: Theme.borderWidth
|
||||
border.color: root.checked ? Theme.accent : Theme.border
|
||||
opacity: root.busy ? 0.6 : 1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.animDuration
|
||||
easing.type: Theme.animEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: knob
|
||||
|
||||
y: (parent.height - height) / 2
|
||||
x: root.checked ? root.width - width - 3 : 3
|
||||
width: root.height - 6
|
||||
height: width
|
||||
radius: height / 2
|
||||
color: root.checked ? Theme.background : mouse.containsMouse ? Theme.text : Theme.textDim
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDuration
|
||||
easing.type: Theme.animEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.animDuration
|
||||
easing.type: Theme.animEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggled(!root.checked)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.config
|
||||
|
||||
// The wifi fan, drawn for the same reason as the bluetooth rune: themes ship it
|
||||
// symbolic-only, which Qt renders black-on-transparent. Drawing it also lets
|
||||
// each arc be lit separately, which is how signal strength is shown.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: Theme.text
|
||||
// Unlit arcs stay drawn, faintly, so the glyph keeps one silhouette at
|
||||
// every strength instead of changing shape as the signal moves.
|
||||
property color dimColor: Theme.textDim
|
||||
property int size: Theme.iconSize
|
||||
// Lit arcs, 0-3.
|
||||
property int level: 3
|
||||
// Struck through with a diagonal, for "off".
|
||||
property bool struck: false
|
||||
|
||||
implicitWidth: root.size
|
||||
implicitHeight: root.size
|
||||
|
||||
readonly property real cx: root.size / 2
|
||||
readonly property real cy: root.size * 0.74
|
||||
readonly property real stroke: Math.max(1.5, root.size * 0.085)
|
||||
|
||||
Rectangle {
|
||||
x: root.cx - width / 2
|
||||
y: root.cy - height / 2
|
||||
width: Math.max(2, root.size * 0.14)
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: root.level > 0 ? root.color : root.dimColor
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: 3
|
||||
|
||||
Shape {
|
||||
id: arc
|
||||
|
||||
required property int index
|
||||
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.level > arc.index ? root.color : root.dimColor
|
||||
strokeWidth: root.stroke
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
// Centred on the dot, opening upwards: 225° to 315°, measured
|
||||
// clockwise from three o'clock.
|
||||
PathAngleArc {
|
||||
centerX: root.cx
|
||||
centerY: root.cy
|
||||
radiusX: root.size * (0.2 + arc.index * 0.115)
|
||||
radiusY: root.size * (0.2 + arc.index * 0.115)
|
||||
startAngle: 225
|
||||
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