mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
switch hto monorepo structure
This commit is contained in:
82
quickshell/Widgets/AppIconRenderer.qml
Normal file
82
quickshell/Widgets/AppIconRenderer.qml
Normal file
@@ -0,0 +1,82 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string iconValue
|
||||
required property int iconSize
|
||||
property string fallbackText: "A"
|
||||
property color iconColor: Theme.surfaceText
|
||||
property color fallbackBackgroundColor: Theme.surfaceLight
|
||||
property color fallbackTextColor: Theme.primary
|
||||
property real materialIconSizeAdjustment: Theme.spacingM
|
||||
property real unicodeIconScale: 0.7
|
||||
property real fallbackTextScale: 0.4
|
||||
property alias iconMargins: iconImg.anchors.margins
|
||||
property real fallbackLeftMargin: 0
|
||||
property real fallbackRightMargin: 0
|
||||
property real fallbackTopMargin: 0
|
||||
property real fallbackBottomMargin: 0
|
||||
|
||||
readonly property bool isMaterial: iconValue.startsWith("material:")
|
||||
readonly property bool isUnicode: iconValue.startsWith("unicode:")
|
||||
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
|
||||
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
|
||||
readonly property string iconPath: isMaterial || isUnicode ? "" : Quickshell.iconPath(iconValue, true) || DesktopService.resolveIconPath(iconValue)
|
||||
|
||||
visible: iconValue !== undefined && iconValue !== ""
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.materialName
|
||||
size: root.iconSize - root.materialIconSizeAdjustment
|
||||
color: root.iconColor
|
||||
visible: root.isMaterial
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: root.unicodeChar
|
||||
font.pixelSize: root.iconSize * root.unicodeIconScale
|
||||
color: root.iconColor
|
||||
visible: root.isUnicode
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
|
||||
anchors.fill: parent
|
||||
source: root.iconPath
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: !root.isMaterial && !root.isUnicode && status === Image.Ready
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: fallbackRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.fallbackLeftMargin
|
||||
anchors.rightMargin: root.fallbackRightMargin
|
||||
anchors.topMargin: root.fallbackTopMargin
|
||||
anchors.bottomMargin: root.fallbackBottomMargin
|
||||
visible: !root.isMaterial && !root.isUnicode && iconImg.status !== Image.Ready
|
||||
color: root.fallbackBackgroundColor
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 0
|
||||
border.color: Theme.primarySelected
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: root.fallbackText
|
||||
font.pixelSize: root.iconSize * root.fallbackTextScale
|
||||
color: root.fallbackTextColor
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
104
quickshell/Widgets/AppLauncherGridDelegate.qml
Normal file
104
quickshell/Widgets/AppLauncherGridDelegate.qml
Normal file
@@ -0,0 +1,104 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var model
|
||||
required property int index
|
||||
required property var gridView
|
||||
property int cellWidth: 120
|
||||
property int cellHeight: 120
|
||||
property int cellPadding: 8
|
||||
property int minIconSize: 32
|
||||
property int maxIconSize: 64
|
||||
property real iconSizeRatio: 0.5
|
||||
property bool hoverUpdatesSelection: true
|
||||
property bool keyboardNavigationActive: false
|
||||
property int currentIndex: -1
|
||||
property bool isPlugin: model?.isPlugin || false
|
||||
property real mouseAreaLeftMargin: 0
|
||||
property real mouseAreaRightMargin: 0
|
||||
property real mouseAreaBottomMargin: 0
|
||||
property real iconFallbackLeftMargin: 0
|
||||
property real iconFallbackRightMargin: 0
|
||||
property real iconFallbackBottomMargin: 0
|
||||
property real iconMaterialSizeAdjustment: 0
|
||||
property real iconUnicodeScale: 0.8
|
||||
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
signal keyboardNavigationReset()
|
||||
|
||||
width: cellWidth - cellPadding
|
||||
height: cellHeight - cellPadding
|
||||
radius: Theme.cornerRadius
|
||||
color: currentIndex === index ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
AppIconRenderer {
|
||||
property int computedIconSize: Math.min(root.maxIconSize, Math.max(root.minIconSize, root.cellWidth * root.iconSizeRatio))
|
||||
|
||||
width: computedIconSize
|
||||
height: computedIconSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
iconValue: model.icon && model.icon !== "" ? model.icon : model.startupClass
|
||||
iconSize: computedIconSize
|
||||
fallbackText: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||
materialIconSizeAdjustment: root.iconMaterialSizeAdjustment
|
||||
unicodeIconScale: root.iconUnicodeScale
|
||||
fallbackTextScale: Math.min(28, computedIconSize * 0.5) / computedIconSize
|
||||
iconMargins: 0
|
||||
fallbackLeftMargin: root.iconFallbackLeftMargin
|
||||
fallbackRightMargin: root.iconFallbackRightMargin
|
||||
fallbackBottomMargin: root.iconFallbackBottomMargin
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: root.cellWidth - 12
|
||||
text: model.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
maximumLineCount: 1
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.mouseAreaLeftMargin
|
||||
anchors.rightMargin: root.mouseAreaRightMargin
|
||||
anchors.bottomMargin: root.mouseAreaBottomMargin
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
z: 10
|
||||
onEntered: {
|
||||
if (root.hoverUpdatesSelection && !root.keyboardNavigationActive)
|
||||
root.gridView.currentIndex = root.index
|
||||
}
|
||||
onPositionChanged: {
|
||||
root.keyboardNavigationReset()
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.itemClicked(root.index, root.model)
|
||||
} else if (mouse.button === Qt.RightButton && !root.isPlugin) {
|
||||
const globalPos = mapToItem(null, mouse.x, mouse.y)
|
||||
root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
quickshell/Widgets/AppLauncherListDelegate.qml
Normal file
114
quickshell/Widgets/AppLauncherListDelegate.qml
Normal file
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var model
|
||||
required property int index
|
||||
required property var listView
|
||||
property int itemHeight: 60
|
||||
property int iconSize: 40
|
||||
property bool showDescription: true
|
||||
property bool hoverUpdatesSelection: true
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool isCurrentItem: false
|
||||
property bool isPlugin: model?.isPlugin || false
|
||||
property real mouseAreaLeftMargin: 0
|
||||
property real mouseAreaRightMargin: 0
|
||||
property real mouseAreaBottomMargin: 0
|
||||
property real iconMargins: 0
|
||||
property real iconFallbackLeftMargin: 0
|
||||
property real iconFallbackRightMargin: 0
|
||||
property real iconFallbackBottomMargin: 0
|
||||
property real iconMaterialSizeAdjustment: Theme.spacingM
|
||||
property real iconUnicodeScale: 0.7
|
||||
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
signal keyboardNavigationReset()
|
||||
|
||||
width: listView.width
|
||||
height: itemHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: isCurrentItem ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingL
|
||||
|
||||
AppIconRenderer {
|
||||
width: root.iconSize
|
||||
height: root.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconValue: model.icon && model.icon !== "" ? model.icon : model.startupClass
|
||||
iconSize: root.iconSize
|
||||
fallbackText: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||
iconMargins: root.iconMargins
|
||||
fallbackLeftMargin: root.iconFallbackLeftMargin
|
||||
fallbackRightMargin: root.iconFallbackRightMargin
|
||||
fallbackBottomMargin: root.iconFallbackBottomMargin
|
||||
materialIconSizeAdjustment: root.iconMaterialSizeAdjustment
|
||||
unicodeIconScale: root.iconUnicodeScale
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: (model.icon !== undefined && model.icon !== "") ? (parent.width - root.iconSize - Theme.spacingL) : parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: model.name || ""
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: model.comment || "Application"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: root.showDescription && model.comment && model.comment.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.mouseAreaLeftMargin
|
||||
anchors.rightMargin: root.mouseAreaRightMargin
|
||||
anchors.bottomMargin: root.mouseAreaBottomMargin
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
z: 10
|
||||
onEntered: {
|
||||
if (root.hoverUpdatesSelection && !root.keyboardNavigationActive)
|
||||
root.listView.currentIndex = root.index
|
||||
}
|
||||
onPositionChanged: {
|
||||
root.keyboardNavigationReset()
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.itemClicked(root.index, root.model)
|
||||
} else if (mouse.button === Qt.RightButton && !root.isPlugin) {
|
||||
const globalPos = mapToItem(null, mouse.x, mouse.y)
|
||||
root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
quickshell/Widgets/CachingImage.qml
Normal file
57
quickshell/Widgets/CachingImage.qml
Normal file
@@ -0,0 +1,57 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Image {
|
||||
id: root
|
||||
|
||||
property string imagePath: ""
|
||||
property string imageHash: ""
|
||||
property int maxCacheSize: 512
|
||||
readonly property string cachePath: imageHash ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
|
||||
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: maxCacheSize
|
||||
sourceSize.height: maxCacheSize
|
||||
smooth: true
|
||||
onImagePathChanged: {
|
||||
if (!imagePath) {
|
||||
source = ""
|
||||
imageHash = ""
|
||||
return
|
||||
}
|
||||
hashProcess.command = ["sha256sum", Paths.strip(imagePath)]
|
||||
hashProcess.running = true
|
||||
}
|
||||
onCachePathChanged: {
|
||||
if (!imageHash || !cachePath)
|
||||
return
|
||||
|
||||
Paths.mkdir(Paths.imagecache)
|
||||
source = cachePath
|
||||
}
|
||||
onStatusChanged: {
|
||||
if (source == cachePath && status === Image.Error) {
|
||||
source = imagePath
|
||||
return
|
||||
}
|
||||
if (source != imagePath || status !== Image.Ready || !imageHash || !cachePath)
|
||||
return
|
||||
|
||||
Paths.mkdir(Paths.imagecache)
|
||||
const grabPath = cachePath
|
||||
if (visible && width > 0 && height > 0 && Window.window && Window.window.visible)
|
||||
grabToImage(res => {
|
||||
return res.saveToFile(grabPath)
|
||||
})
|
||||
}
|
||||
|
||||
Process {
|
||||
id: hashProcess
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.imageHash = text.split(" ")[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
38
quickshell/Widgets/DankActionButton.qml
Normal file
38
quickshell/Widgets/DankActionButton.qml
Normal file
@@ -0,0 +1,38 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
property string iconName: ""
|
||||
property int iconSize: Theme.iconSize - 4
|
||||
property color iconColor: Theme.surfaceText
|
||||
property color backgroundColor: "transparent"
|
||||
property bool circular: true
|
||||
property int buttonSize: 32
|
||||
|
||||
signal clicked
|
||||
signal entered
|
||||
signal exited
|
||||
|
||||
width: buttonSize
|
||||
height: buttonSize
|
||||
radius: Theme.cornerRadius
|
||||
color: backgroundColor
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.iconName
|
||||
size: root.iconSize
|
||||
color: root.iconColor
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: root.radius
|
||||
onClicked: root.clicked()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
}
|
||||
}
|
||||
168
quickshell/Widgets/DankAlbumArt.qml
Normal file
168
quickshell/Widgets/DankAlbumArt.qml
Normal file
@@ -0,0 +1,168 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Shapes
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property MprisPlayer activePlayer
|
||||
property string artUrl: (activePlayer?.trackArtUrl) || ""
|
||||
property string lastValidArtUrl: ""
|
||||
property alias albumArtStatus: albumArt.imageStatus
|
||||
property real albumSize: Math.min(width, height) * 0.88
|
||||
property bool showAnimation: true
|
||||
property real animationScale: 1.0
|
||||
|
||||
onArtUrlChanged: {
|
||||
if (artUrl && albumArt.status !== Image.Error) {
|
||||
lastValidArtUrl = artUrl
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
||||
sourceComponent: Component {
|
||||
Ref {
|
||||
service: CavaService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: morphingBlob
|
||||
width: parent.width * 1.1
|
||||
height: parent.height * 1.1
|
||||
anchors.centerIn: parent
|
||||
visible: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
||||
asynchronous: false
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
z: 0
|
||||
layer.enabled: false
|
||||
|
||||
readonly property real centerX: width / 2
|
||||
readonly property real centerY: height / 2
|
||||
readonly property real baseRadius: Math.min(width, height) * 0.41 * root.animationScale
|
||||
readonly property int segments: 28
|
||||
|
||||
property var audioLevels: {
|
||||
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
|
||||
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
|
||||
}
|
||||
return CavaService.values
|
||||
}
|
||||
|
||||
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
|
||||
property var cubics: []
|
||||
|
||||
onAudioLevelsChanged: updatePath()
|
||||
|
||||
FrameAnimation {
|
||||
running: morphingBlob.visible
|
||||
onTriggered: morphingBlob.updatePath()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: cubicSegment
|
||||
PathCubic {}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
shapePath.pathElements.push(Qt.createQmlObject(
|
||||
'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath
|
||||
))
|
||||
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const seg = cubicSegment.createObject(shapePath)
|
||||
shapePath.pathElements.push(seg)
|
||||
cubics.push(seg)
|
||||
}
|
||||
|
||||
updatePath()
|
||||
}
|
||||
|
||||
function expSmooth(prev, next, alpha) {
|
||||
return prev + alpha * (next - prev)
|
||||
}
|
||||
|
||||
function updatePath() {
|
||||
if (cubics.length === 0) return
|
||||
|
||||
for (let i = 0; i < Math.min(smoothedLevels.length, audioLevels.length); i++) {
|
||||
smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 0.35)
|
||||
}
|
||||
|
||||
const points = []
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const angle = (i / segments) * 2 * Math.PI
|
||||
const audioIndex = i % Math.min(smoothedLevels.length, 10)
|
||||
|
||||
const rawLevel = smoothedLevels[audioIndex] || 0
|
||||
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100
|
||||
const normalizedLevel = scaledLevel / 100
|
||||
const audioLevel = Math.max(0.15, normalizedLevel) * 0.5
|
||||
|
||||
const radius = baseRadius * (1.0 + audioLevel)
|
||||
const x = centerX + Math.cos(angle) * radius
|
||||
const y = centerY + Math.sin(angle) * radius
|
||||
points.push({x: x, y: y})
|
||||
}
|
||||
|
||||
const startMove = shapePath.pathElements[0]
|
||||
startMove.x = points[0].x
|
||||
startMove.y = points[0].y
|
||||
|
||||
const tension = 0.5
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const p0 = points[(i - 1 + segments) % segments]
|
||||
const p1 = points[i]
|
||||
const p2 = points[(i + 1) % segments]
|
||||
const p3 = points[(i + 2) % segments]
|
||||
|
||||
const c1x = p1.x + (p2.x - p0.x) * tension / 3
|
||||
const c1y = p1.y + (p2.y - p0.y) * tension / 3
|
||||
const c2x = p2.x - (p3.x - p1.x) * tension / 3
|
||||
const c2y = p2.y - (p3.y - p1.y) * tension / 3
|
||||
|
||||
const seg = cubics[i]
|
||||
seg.control1X = c1x
|
||||
seg.control1Y = c1y
|
||||
seg.control2X = c2x
|
||||
seg.control2Y = c2y
|
||||
seg.x = p2.x
|
||||
seg.y = p2.y
|
||||
}
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
id: shapePath
|
||||
fillColor: Theme.primary
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillRule: ShapePath.WindingFill
|
||||
}
|
||||
}
|
||||
|
||||
DankCircularImage {
|
||||
id: albumArt
|
||||
width: albumSize
|
||||
height: albumSize
|
||||
anchors.centerIn: parent
|
||||
z: 1
|
||||
|
||||
imageSource: artUrl || lastValidArtUrl || ""
|
||||
fallbackIcon: "album"
|
||||
border.color: Theme.primary
|
||||
border.width: 2
|
||||
|
||||
onImageSourceChanged: {
|
||||
if (imageSource && imageStatus !== Image.Error) {
|
||||
lastValidArtUrl = imageSource
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
quickshell/Widgets/DankBackdrop.qml
Normal file
64
quickshell/Widgets/DankBackdrop.qml
Normal file
@@ -0,0 +1,64 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
property string screenName: ""
|
||||
property bool isColorWallpaper: {
|
||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
|
||||
return currentWallpaper && currentWallpaper.startsWith("#")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: isColorWallpaper ? SessionData.getMonitorWallpaper(screenName) : Theme.background
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: parent.width * 0.7
|
||||
y: -parent.height * 0.3
|
||||
width: parent.width * 0.8
|
||||
height: parent.height * 1.5
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
|
||||
rotation: 35
|
||||
visible: !isColorWallpaper
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: parent.width * 0.85
|
||||
y: -parent.height * 0.2
|
||||
width: parent.width * 0.4
|
||||
height: parent.height * 1.2
|
||||
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12)
|
||||
rotation: 35
|
||||
visible: !isColorWallpaper
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: Theme.spacingXL * 2
|
||||
anchors.bottomMargin: Theme.spacingXL * 2
|
||||
width: 200
|
||||
height: width * (569.94629 / 506.50931)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
source: "file://" + Theme.shellDir + "/assets/danklogonormal.svg"
|
||||
opacity: 0.25
|
||||
visible: !isColorWallpaper
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.mipmap: true
|
||||
layer.effect: MultiEffect {
|
||||
saturation: 0
|
||||
colorization: 1
|
||||
colorizationColor: Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
75
quickshell/Widgets/DankButton.qml
Normal file
75
quickshell/Widgets/DankButton.qml
Normal file
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property string iconName: ""
|
||||
property int iconSize: Theme.iconSizeSmall
|
||||
property bool enabled: true
|
||||
property bool hovered: mouseArea.containsMouse
|
||||
property bool pressed: mouseArea.pressed
|
||||
property color backgroundColor: Theme.primary
|
||||
property color textColor: Theme.primaryText
|
||||
property int buttonHeight: 40
|
||||
property int horizontalPadding: Theme.spacingL
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: Math.max(contentRow.implicitWidth + horizontalPadding * 2, 64)
|
||||
height: buttonHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: backgroundColor
|
||||
opacity: enabled ? 1 : 0.4
|
||||
|
||||
Rectangle {
|
||||
id: stateLayer
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: {
|
||||
if (pressed) return Theme.primaryPressed
|
||||
if (hovered) return Theme.primaryHover
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shorterDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: root.iconSize
|
||||
color: root.textColor
|
||||
visible: root.iconName !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: root.textColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: root.enabled
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
213
quickshell/Widgets/DankButtonGroup.qml
Normal file
213
quickshell/Widgets/DankButtonGroup.qml
Normal file
@@ -0,0 +1,213 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
|
||||
property var model: []
|
||||
property int currentIndex: -1
|
||||
property string selectionMode: "single"
|
||||
property bool multiSelect: selectionMode === "multi"
|
||||
property var initialSelection: []
|
||||
property var currentSelection: initialSelection
|
||||
property bool checkEnabled: true
|
||||
property int buttonHeight: 40
|
||||
property int minButtonWidth: 64
|
||||
property int buttonPadding: Theme.spacingL
|
||||
property int checkIconSize: Theme.iconSizeSmall
|
||||
property int textSize: Theme.fontSizeMedium
|
||||
|
||||
signal selectionChanged(int index, bool selected)
|
||||
signal animationCompleted()
|
||||
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Timer {
|
||||
id: animationTimer
|
||||
interval: Theme.shortDuration
|
||||
onTriggered: root.animationCompleted()
|
||||
}
|
||||
|
||||
function isSelected(index) {
|
||||
if (multiSelect) {
|
||||
return repeater.itemAt(index)?.selected || false
|
||||
}
|
||||
return index === currentIndex
|
||||
}
|
||||
|
||||
function selectItem(index) {
|
||||
if (multiSelect) {
|
||||
const modelValue = model[index]
|
||||
let newSelection = [...currentSelection]
|
||||
const isCurrentlySelected = newSelection.includes(modelValue)
|
||||
|
||||
if (isCurrentlySelected) {
|
||||
newSelection = newSelection.filter(item => item !== modelValue)
|
||||
} else {
|
||||
newSelection.push(modelValue)
|
||||
}
|
||||
|
||||
currentSelection = newSelection
|
||||
selectionChanged(index, !isCurrentlySelected)
|
||||
animationTimer.restart()
|
||||
} else {
|
||||
const oldIndex = currentIndex
|
||||
currentIndex = index
|
||||
selectionChanged(index, true)
|
||||
if (oldIndex !== index && oldIndex >= 0) {
|
||||
selectionChanged(oldIndex, false)
|
||||
}
|
||||
animationTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: ScriptModel {
|
||||
values: root.model
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: segment
|
||||
|
||||
property bool selected: multiSelect ? root.currentSelection.includes(modelData) : (index === root.currentIndex)
|
||||
property bool hovered: mouseArea.containsMouse
|
||||
property bool pressed: mouseArea.pressed
|
||||
property bool isFirst: index === 0
|
||||
property bool isLast: index === repeater.count - 1
|
||||
property bool prevSelected: index > 0 ? root.isSelected(index - 1) : false
|
||||
property bool nextSelected: index < repeater.count - 1 ? root.isSelected(index + 1) : false
|
||||
|
||||
width: Math.max(contentItem.implicitWidth + root.buttonPadding * 2, root.minButtonWidth) + (selected ? 4 : 0)
|
||||
height: root.buttonHeight
|
||||
|
||||
color: selected ? Theme.primary : Theme.surfaceVariant
|
||||
border.color: "transparent"
|
||||
border.width: 0
|
||||
|
||||
topLeftRadius: (isFirst || selected) ? Theme.cornerRadius : 4
|
||||
bottomLeftRadius: (isFirst || selected) ? Theme.cornerRadius : 4
|
||||
topRightRadius: (isLast || selected) ? Theme.cornerRadius : 4
|
||||
bottomRightRadius: (isLast || selected) ? Theme.cornerRadius : 4
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on topLeftRadius {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on topRightRadius {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on bottomLeftRadius {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on bottomRightRadius {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stateLayer
|
||||
anchors.fill: parent
|
||||
topLeftRadius: parent.topLeftRadius
|
||||
bottomLeftRadius: parent.bottomLeftRadius
|
||||
topRightRadius: parent.topRightRadius
|
||||
bottomRightRadius: parent.bottomRightRadius
|
||||
color: {
|
||||
if (pressed) return selected ? Theme.primaryPressed : Theme.surfaceTextHover
|
||||
if (hovered) return selected ? Theme.primaryHover : Theme.surfaceTextHover
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shorterDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentItem
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: contentRow.implicitWidth
|
||||
implicitHeight: contentRow.implicitHeight
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
id: checkIcon
|
||||
name: "check"
|
||||
size: root.checkIconSize
|
||||
color: segment.selected ? Theme.primaryText : Theme.surfaceVariantText
|
||||
visible: root.checkEnabled && segment.selected
|
||||
opacity: segment.selected ? 1 : 0
|
||||
scale: segment.selected ? 1 : 0.6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: buttonText
|
||||
text: typeof modelData === "string" ? modelData : modelData.text || ""
|
||||
font.pixelSize: root.textSize
|
||||
font.weight: segment.selected ? Font.Medium : Font.Normal
|
||||
color: segment.selected ? Theme.primaryText : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.selectItem(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
quickshell/Widgets/DankCircularImage.qml
Normal file
79
quickshell/Widgets/DankCircularImage.qml
Normal file
@@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string imageSource: ""
|
||||
property string fallbackIcon: "notifications"
|
||||
property string fallbackText: ""
|
||||
property bool hasImage: imageSource !== ""
|
||||
property alias imageStatus: internalImage.status
|
||||
|
||||
radius: width / 2
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
border.color: "transparent"
|
||||
border.width: 0
|
||||
|
||||
Image {
|
||||
id: internalImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: true
|
||||
visible: false
|
||||
source: root.imageSource
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
source: internalImage
|
||||
maskEnabled: true
|
||||
maskSource: circularMask
|
||||
visible: internalImage.status === Image.Ready && root.imageSource !== ""
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: circularMask
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 4
|
||||
height: parent.height - 4
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.fallbackIcon
|
||||
size: parent.width * 0.5
|
||||
color: Theme.surfaceVariantText
|
||||
visible: (internalImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== ""
|
||||
}
|
||||
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== ""
|
||||
text: root.fallbackText
|
||||
font.pixelSize: Math.max(12, parent.width * 0.5)
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
375
quickshell/Widgets/DankDropdown.qml
Normal file
375
quickshell/Widgets/DankDropdown.qml
Normal file
@@ -0,0 +1,375 @@
|
||||
import "../Common/fzf.js" as Fzf
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property string description: ""
|
||||
property string currentValue: ""
|
||||
property var options: []
|
||||
property var optionIcons: []
|
||||
property bool enableFuzzySearch: false
|
||||
property int popupWidthOffset: 0
|
||||
property int maxPopupHeight: 400
|
||||
property bool openUpwards: false
|
||||
property int popupWidth: 0
|
||||
property bool alignPopupRight: false
|
||||
property int dropdownWidth: 200
|
||||
property bool compactMode: text === "" && description === ""
|
||||
property bool addHorizontalPadding: false
|
||||
|
||||
signal valueChanged(string value)
|
||||
|
||||
width: compactMode ? dropdownWidth : parent.width
|
||||
implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
|
||||
|
||||
Component.onDestruction: {
|
||||
const popup = dropdownMenu
|
||||
if (popup && popup.visible) {
|
||||
popup.close()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: labelColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: dropdown.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: root.addHorizontalPadding ? Theme.spacingM : 0
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
visible: !root.compactMode
|
||||
|
||||
StyledText {
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: description.length > 0
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dropdown
|
||||
|
||||
width: root.compactMode ? parent.width : (root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth))
|
||||
height: 40
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.addHorizontalPadding && !root.compactMode ? Theme.spacingM : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Theme.cornerRadius
|
||||
color: dropdownArea.containsMouse || dropdownMenu.visible ? Theme.surfaceContainerHigh : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: dropdownMenu.visible ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: dropdownMenu.visible ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
id: dropdownArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (dropdownMenu.visible) {
|
||||
dropdownMenu.close()
|
||||
return
|
||||
}
|
||||
|
||||
dropdownMenu.searchQuery = ""
|
||||
dropdownMenu.updateFilteredOptions()
|
||||
|
||||
dropdownMenu.open()
|
||||
|
||||
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0)
|
||||
const popupWidth = dropdownMenu.width
|
||||
const popupHeight = dropdownMenu.height
|
||||
const overlayHeight = Overlay.overlay.height
|
||||
|
||||
if (root.openUpwards || pos.y + dropdown.height + popupHeight + 4 > overlayHeight) {
|
||||
if (root.alignPopupRight) {
|
||||
dropdownMenu.x = pos.x + dropdown.width - popupWidth
|
||||
} else {
|
||||
dropdownMenu.x = pos.x - (root.popupWidthOffset / 2)
|
||||
}
|
||||
dropdownMenu.y = pos.y - popupHeight - 4
|
||||
} else {
|
||||
if (root.alignPopupRight) {
|
||||
dropdownMenu.x = pos.x + dropdown.width - popupWidth
|
||||
} else {
|
||||
dropdownMenu.x = pos.x - (root.popupWidthOffset / 2)
|
||||
}
|
||||
dropdownMenu.y = pos.y + dropdown.height + 4
|
||||
}
|
||||
|
||||
if (root.enableFuzzySearch && searchField.visible) {
|
||||
searchField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: expandIcon.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const currentIndex = root.options.indexOf(root.currentValue)
|
||||
return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : ""
|
||||
}
|
||||
size: 18
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: name !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.currentValue
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: contentRow.width - (contentRow.children[0].visible ? contentRow.children[0].width + contentRow.spacing : 0)
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: expandIcon
|
||||
|
||||
name: dropdownMenu.visible ? "expand_less" : "expand_more"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: dropdownMenu
|
||||
|
||||
property string searchQuery: ""
|
||||
property var filteredOptions: []
|
||||
property int selectedIndex: -1
|
||||
property var fzfFinder: new Fzf.Finder(root.options, {
|
||||
"selector": option => option,
|
||||
"limit": 50,
|
||||
"casing": "case-insensitive"
|
||||
})
|
||||
|
||||
function updateFilteredOptions() {
|
||||
if (!root.enableFuzzySearch || searchQuery.length === 0) {
|
||||
filteredOptions = root.options
|
||||
selectedIndex = -1
|
||||
return
|
||||
}
|
||||
|
||||
const results = fzfFinder.find(searchQuery)
|
||||
filteredOptions = results.map(result => result.item)
|
||||
selectedIndex = -1
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredOptions.length === 0) {
|
||||
return
|
||||
}
|
||||
selectedIndex = (selectedIndex + 1) % filteredOptions.length
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredOptions.length === 0) {
|
||||
return
|
||||
}
|
||||
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
function selectCurrent() {
|
||||
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) {
|
||||
return
|
||||
}
|
||||
root.currentValue = filteredOptions[selectedIndex]
|
||||
root.valueChanged(filteredOptions[selectedIndex])
|
||||
close()
|
||||
}
|
||||
|
||||
parent: Overlay.overlay
|
||||
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset))
|
||||
height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16)
|
||||
padding: 0
|
||||
modal: true
|
||||
dim: false
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
|
||||
border.color: Theme.primary
|
||||
border.width: 2
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowBlur: 0.4
|
||||
shadowColor: Theme.shadowStrong
|
||||
shadowVerticalOffset: 4
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: searchContainer
|
||||
|
||||
width: parent.width
|
||||
height: 42
|
||||
visible: root.enableFuzzySearch
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
placeholderText: I18n.tr("Search...")
|
||||
text: dropdownMenu.searchQuery
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
onTextChanged: {
|
||||
dropdownMenu.searchQuery = text
|
||||
dropdownMenu.updateFilteredOptions()
|
||||
}
|
||||
Keys.onDownPressed: dropdownMenu.selectNext()
|
||||
Keys.onUpPressed: dropdownMenu.selectPrevious()
|
||||
Keys.onReturnPressed: dropdownMenu.selectCurrent()
|
||||
Keys.onEnterPressed: dropdownMenu.selectCurrent()
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
dropdownMenu.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
dropdownMenu.selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
dropdownMenu.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
dropdownMenu.selectPrevious()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingXS
|
||||
visible: root.enableFuzzySearch
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: listView
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0)
|
||||
clip: true
|
||||
model: ScriptModel {
|
||||
values: dropdownMenu.filteredOptions
|
||||
}
|
||||
spacing: 2
|
||||
|
||||
interactive: true
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
delegate: Rectangle {
|
||||
property bool isSelected: dropdownMenu.selectedIndex === index
|
||||
property bool isCurrentValue: root.currentValue === modelData
|
||||
property int optionIndex: root.options.indexOf(modelData)
|
||||
|
||||
width: ListView.view.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : ""
|
||||
size: 18
|
||||
color: isCurrentValue ? Theme.primary : Theme.surfaceText
|
||||
visible: name !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: isCurrentValue ? Theme.primary : Theme.surfaceText
|
||||
font.weight: isCurrentValue ? Font.Medium : Font.Normal
|
||||
width: root.popupWidth > 0 ? undefined : (parent.parent.width - parent.x - Theme.spacingS)
|
||||
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: optionArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.currentValue = modelData
|
||||
root.valueChanged(modelData)
|
||||
dropdownMenu.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
173
quickshell/Widgets/DankFlickable.qml
Normal file
173
quickshell/Widgets/DankFlickable.qml
Normal file
@@ -0,0 +1,173 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
|
||||
property real mouseWheelSpeed: 60
|
||||
property real momentumVelocity: 0
|
||||
property bool isMomentumActive: false
|
||||
property real friction: 0.95
|
||||
property real minMomentumVelocity: 50
|
||||
property real maxMomentumVelocity: 2500
|
||||
property bool _scrollBarActive: false
|
||||
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
|
||||
property real touchpadSpeed: 1.8
|
||||
property real momentumRetention: 0.92
|
||||
property real lastWheelTime: 0
|
||||
property real momentum: 0
|
||||
property var velocitySamples: []
|
||||
property bool sessionUsedMouseWheel: false
|
||||
|
||||
function startMomentum() {
|
||||
flickable.isMomentumActive = true
|
||||
momentumTimer.start()
|
||||
}
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
|
||||
onWheel: event => {
|
||||
vbar._scrollBarActive = true
|
||||
vbar.hideTimer.restart()
|
||||
|
||||
const currentTime = Date.now()
|
||||
const timeDelta = currentTime - lastWheelTime
|
||||
lastWheelTime = currentTime
|
||||
|
||||
const deltaY = event.angleDelta.y
|
||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
|
||||
|
||||
if (isMouseWheel) {
|
||||
sessionUsedMouseWheel = true
|
||||
momentumTimer.stop()
|
||||
flickable.isMomentumActive = false
|
||||
velocitySamples = []
|
||||
momentum = 0
|
||||
flickable.momentumVelocity = 0
|
||||
|
||||
const lines = Math.floor(Math.abs(deltaY) / 120)
|
||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed
|
||||
let newY = flickable.contentY + scrollAmount
|
||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
|
||||
|
||||
if (flickable.flicking) {
|
||||
flickable.cancelFlick()
|
||||
}
|
||||
|
||||
flickable.contentY = newY
|
||||
} else {
|
||||
sessionUsedMouseWheel = false
|
||||
momentumTimer.stop()
|
||||
flickable.isMomentumActive = false
|
||||
|
||||
let delta = 0
|
||||
if (event.pixelDelta.y !== 0) {
|
||||
delta = event.pixelDelta.y * touchpadSpeed
|
||||
} else {
|
||||
delta = event.angleDelta.y / 8 * touchpadSpeed
|
||||
}
|
||||
|
||||
velocitySamples.push({
|
||||
"delta": delta,
|
||||
"time": currentTime
|
||||
})
|
||||
velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100)
|
||||
|
||||
if (velocitySamples.length > 1) {
|
||||
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0)
|
||||
const timeSpan = currentTime - velocitySamples[0].time
|
||||
if (timeSpan > 0) {
|
||||
flickable.momentumVelocity = Math.max(-flickable.maxMomentumVelocity, Math.min(flickable.maxMomentumVelocity, totalDelta / timeSpan * 1000))
|
||||
}
|
||||
}
|
||||
|
||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
||||
momentum = momentum * momentumRetention + delta * 0.15
|
||||
delta += momentum
|
||||
} else {
|
||||
momentum = 0
|
||||
}
|
||||
|
||||
let newY = flickable.contentY - delta
|
||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
|
||||
|
||||
if (flickable.flicking) {
|
||||
flickable.cancelFlick()
|
||||
}
|
||||
|
||||
flickable.contentY = newY
|
||||
}
|
||||
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
if (!sessionUsedMouseWheel && Math.abs(flickable.momentumVelocity) >= flickable.minMomentumVelocity) {
|
||||
startMomentum()
|
||||
} else {
|
||||
velocitySamples = []
|
||||
flickable.momentumVelocity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMovementStarted: {
|
||||
vbar._scrollBarActive = true
|
||||
vbar.hideTimer.stop()
|
||||
}
|
||||
onMovementEnded: vbar.hideTimer.restart()
|
||||
|
||||
Timer {
|
||||
id: momentumTimer
|
||||
interval: 16
|
||||
repeat: true
|
||||
|
||||
onTriggered: {
|
||||
const newY = flickable.contentY - flickable.momentumVelocity * 0.016
|
||||
const maxY = Math.max(0, flickable.contentHeight - flickable.height)
|
||||
|
||||
if (newY < 0 || newY > maxY) {
|
||||
flickable.contentY = newY < 0 ? 0 : maxY
|
||||
stop()
|
||||
flickable.isMomentumActive = false
|
||||
flickable.momentumVelocity = 0
|
||||
return
|
||||
}
|
||||
|
||||
flickable.contentY = newY
|
||||
flickable.momentumVelocity *= flickable.friction
|
||||
|
||||
if (Math.abs(flickable.momentumVelocity) < 5) {
|
||||
stop()
|
||||
flickable.isMomentumActive = false
|
||||
flickable.momentumVelocity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: returnToBoundsAnimation
|
||||
target: flickable
|
||||
property: "contentY"
|
||||
duration: 300
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: vbar
|
||||
}
|
||||
}
|
||||
163
quickshell/Widgets/DankGridView.qml
Normal file
163
quickshell/Widgets/DankGridView.qml
Normal file
@@ -0,0 +1,163 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Widgets
|
||||
|
||||
GridView {
|
||||
id: gridView
|
||||
|
||||
property real momentumVelocity: 0
|
||||
property bool isMomentumActive: false
|
||||
property real friction: 0.95
|
||||
property real minMomentumVelocity: 50
|
||||
property real maxMomentumVelocity: 2500
|
||||
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
onMovementStarted: {
|
||||
vbar._scrollBarActive = true
|
||||
vbar.hideTimer.stop()
|
||||
}
|
||||
onMovementEnded: vbar.hideTimer.restart()
|
||||
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
|
||||
property real mouseWheelSpeed: 60
|
||||
property real touchpadSpeed: 1.8
|
||||
property real momentumRetention: 0.92
|
||||
property real lastWheelTime: 0
|
||||
property real momentum: 0
|
||||
property var velocitySamples: []
|
||||
property bool sessionUsedMouseWheel: false
|
||||
|
||||
function startMomentum() {
|
||||
isMomentumActive = true
|
||||
momentumTimer.start()
|
||||
}
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => {
|
||||
vbar._scrollBarActive = true
|
||||
vbar.hideTimer.restart()
|
||||
|
||||
const currentTime = Date.now()
|
||||
const timeDelta = currentTime - lastWheelTime
|
||||
lastWheelTime = currentTime
|
||||
|
||||
const deltaY = event.angleDelta.y
|
||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
|
||||
|
||||
if (isMouseWheel) {
|
||||
sessionUsedMouseWheel = true
|
||||
momentumTimer.stop()
|
||||
isMomentumActive = false
|
||||
velocitySamples = []
|
||||
momentum = 0
|
||||
momentumVelocity = 0
|
||||
|
||||
const lines = Math.floor(Math.abs(deltaY) / 120)
|
||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35
|
||||
let newY = contentY + scrollAmount
|
||||
newY = Math.max(0, Math.min(contentHeight - height, newY))
|
||||
|
||||
if (flicking) {
|
||||
cancelFlick()
|
||||
}
|
||||
|
||||
contentY = newY
|
||||
} else {
|
||||
sessionUsedMouseWheel = false
|
||||
momentumTimer.stop()
|
||||
isMomentumActive = false
|
||||
|
||||
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * touchpadSpeed : event.angleDelta.y / 120 * cellHeight * 1.2
|
||||
|
||||
velocitySamples.push({
|
||||
"delta": delta,
|
||||
"time": currentTime
|
||||
})
|
||||
velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100)
|
||||
|
||||
if (velocitySamples.length > 1) {
|
||||
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0)
|
||||
const timeSpan = currentTime - velocitySamples[0].time
|
||||
if (timeSpan > 0) {
|
||||
momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000))
|
||||
}
|
||||
}
|
||||
|
||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
||||
momentum = momentum * momentumRetention + delta * 0.15
|
||||
delta += momentum
|
||||
} else {
|
||||
momentum = 0
|
||||
}
|
||||
|
||||
let newY = contentY - delta
|
||||
newY = Math.max(0, Math.min(contentHeight - height, newY))
|
||||
|
||||
if (flicking) {
|
||||
cancelFlick()
|
||||
}
|
||||
|
||||
contentY = newY
|
||||
}
|
||||
|
||||
event.accepted = true
|
||||
}
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||
startMomentum()
|
||||
} else {
|
||||
velocitySamples = []
|
||||
momentumVelocity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: momentumTimer
|
||||
interval: 16
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
const newY = contentY - momentumVelocity * 0.016
|
||||
const maxY = Math.max(0, contentHeight - height)
|
||||
|
||||
if (newY < 0 || newY > maxY) {
|
||||
contentY = newY < 0 ? 0 : maxY
|
||||
stop()
|
||||
isMomentumActive = false
|
||||
momentumVelocity = 0
|
||||
return
|
||||
}
|
||||
|
||||
contentY = newY
|
||||
momentumVelocity *= friction
|
||||
|
||||
if (Math.abs(momentumVelocity) < 5) {
|
||||
stop()
|
||||
isMomentumActive = false
|
||||
momentumVelocity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: returnToBoundsAnimation
|
||||
target: gridView
|
||||
property: "contentY"
|
||||
duration: 300
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: vbar
|
||||
}
|
||||
}
|
||||
69
quickshell/Widgets/DankIcon.qml
Normal file
69
quickshell/Widgets/DankIcon.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property alias name: icon.text
|
||||
property alias size: icon.font.pixelSize
|
||||
property alias color: icon.color
|
||||
property bool filled: false
|
||||
property real fill: filled ? 1.0 : 0.0
|
||||
property int grade: Theme.isLightMode ? 0 : -25
|
||||
property int weight: filled ? 500 : 400
|
||||
|
||||
implicitWidth: icon.implicitWidth
|
||||
implicitHeight: icon.implicitHeight
|
||||
|
||||
signal rotationCompleted()
|
||||
|
||||
FontLoader {
|
||||
id: materialSymbolsFont
|
||||
source: Qt.resolvedUrl("../assets/fonts/material-design-icons/variablefont/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: icon
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
font.family: materialSymbolsFont.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: root.weight
|
||||
color: Theme.surfaceText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
antialiasing: true
|
||||
font.variableAxes: {
|
||||
"FILL": root.fill.toFixed(1),
|
||||
"GRAD": root.grade,
|
||||
"opsz": 24,
|
||||
"wght": root.weight
|
||||
}
|
||||
|
||||
Behavior on font.weight {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on fill {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: rotationTimer
|
||||
interval: 16
|
||||
repeat: false
|
||||
onTriggered: root.rotationCompleted()
|
||||
}
|
||||
|
||||
onRotationChanged: {
|
||||
rotationTimer.restart()
|
||||
}
|
||||
}
|
||||
291
quickshell/Widgets/DankIconPicker.qml
Normal file
291
quickshell/Widgets/DankIconPicker.qml
Normal file
@@ -0,0 +1,291 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string currentIcon: ""
|
||||
property string iconType: "icon" // "icon" or "text"
|
||||
|
||||
signal iconSelected(string iconName, string iconType)
|
||||
|
||||
width: 240
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: dropdownLoader.active ? Theme.primary : Theme.outline
|
||||
border.width: 1
|
||||
|
||||
property var iconCategories: [{
|
||||
"name": I18n.tr("Numbers"),
|
||||
"icons": ["looks_one", "looks_two", "looks_3", "looks_4", "looks_5", "looks_6", "filter_1", "filter_2", "filter_3", "filter_4", "filter_5", "filter_6", "filter_7", "filter_8", "filter_9", "filter_9_plus", "plus_one", "exposure_plus_1", "exposure_plus_2"]
|
||||
}, {
|
||||
"name": I18n.tr("Workspace"),
|
||||
"icons": ["work", "laptop", "desktop_windows", "folder", "view_module", "dashboard", "apps", "grid_view"]
|
||||
}, {
|
||||
"name": I18n.tr("Development"),
|
||||
"icons": ["code", "terminal", "bug_report", "build", "engineering", "integration_instructions", "data_object", "schema", "api", "webhook"]
|
||||
}, {
|
||||
"name": I18n.tr("Communication"),
|
||||
"icons": ["chat", "mail", "forum", "message", "video_call", "call", "contacts", "group", "notifications", "campaign"]
|
||||
}, {
|
||||
"name": I18n.tr("Media"),
|
||||
"icons": ["music_note", "headphones", "mic", "videocam", "photo", "movie", "library_music", "album", "radio", "volume_up"]
|
||||
}, {
|
||||
"name": I18n.tr("System"),
|
||||
"icons": ["memory", "storage", "developer_board", "monitor", "keyboard", "mouse", "battery_std", "wifi", "bluetooth", "security", "settings"]
|
||||
}, {
|
||||
"name": I18n.tr("Navigation"),
|
||||
"icons": ["home", "arrow_forward", "arrow_back", "expand_more", "expand_less", "menu", "close", "search", "filter_list", "sort"]
|
||||
}, {
|
||||
"name": I18n.tr("Actions"),
|
||||
"icons": ["add", "remove", "edit", "delete", "save", "download", "upload", "share", "content_copy", "content_paste", "content_cut", "undo", "redo"]
|
||||
}, {
|
||||
"name": I18n.tr("Status"),
|
||||
"icons": ["check", "error", "warning", "info", "done", "pending", "schedule", "update", "sync", "offline_bolt"]
|
||||
}, {
|
||||
"name": I18n.tr("Fun"),
|
||||
"icons": ["celebration", "cake", "star", "favorite", "pets", "sports_esports", "local_fire_department", "bolt", "auto_awesome", "diamond"]
|
||||
}]
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: (root.iconType === "icon" && root.currentIcon) ? root.currentIcon : (root.iconType === "text" ? "text_fields" : "add")
|
||||
size: 16
|
||||
color: root.currentIcon ? Theme.surfaceText : Theme.outline
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.currentIcon ? root.currentIcon : I18n.tr("Choose icon")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.currentIcon ? Theme.surfaceText : Theme.outline
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 160
|
||||
elide: Text.ElideRight
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
dropdownLoader.active = !dropdownLoader.active
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: dropdownLoader.active ? "expand_less" : "expand_more"
|
||||
size: 16
|
||||
color: Theme.outline
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dropdownLoader
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: dropdownPopup
|
||||
|
||||
visible: true
|
||||
implicitWidth: 320
|
||||
implicitHeight: Math.min(500, dropdownContent.implicitHeight + 32)
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Top area - above popup
|
||||
MouseArea {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: popupContainer.y
|
||||
onClicked: {
|
||||
dropdownLoader.active = false
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom area - below popup
|
||||
MouseArea {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: popupContainer.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
onClicked: {
|
||||
dropdownLoader.active = false
|
||||
}
|
||||
}
|
||||
|
||||
// Left area - left of popup
|
||||
MouseArea {
|
||||
anchors.left: parent.left
|
||||
anchors.top: popupContainer.top
|
||||
anchors.bottom: popupContainer.bottom
|
||||
width: popupContainer.x
|
||||
onClicked: {
|
||||
dropdownLoader.active = false
|
||||
}
|
||||
}
|
||||
|
||||
// Right area - right of popup
|
||||
MouseArea {
|
||||
anchors.right: parent.right
|
||||
anchors.top: popupContainer.top
|
||||
anchors.bottom: popupContainer.bottom
|
||||
anchors.left: popupContainer.right
|
||||
onClicked: {
|
||||
dropdownLoader.active = false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: popupContainer
|
||||
width: 320
|
||||
height: Math.min(500, dropdownContent.implicitHeight + 32)
|
||||
x: Math.max(16, Math.min(root.mapToItem(null, 0, 0).x, parent.width - width - 16))
|
||||
y: Math.max(16, Math.min(root.mapToItem(null, 0, root.height + 4).y, parent.height - height - 16))
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surface
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowColor: Theme.shadowStrong
|
||||
shadowBlur: 0.8
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
}
|
||||
|
||||
// Close button
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: closeMouseArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
z: 1
|
||||
|
||||
DankIcon {
|
||||
name: "close"
|
||||
size: 16
|
||||
color: closeMouseArea.containsMouse ? Theme.error : Theme.outline
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
dropdownLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
contentHeight: dropdownContent.height
|
||||
clip: true
|
||||
pressDelay: 0
|
||||
|
||||
Column {
|
||||
id: dropdownContent
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Icon categories
|
||||
Repeater {
|
||||
model: root.iconCategories
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: modelData.icons
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: iconMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
border.color: root.currentIcon === modelData ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: 2
|
||||
|
||||
DankIcon {
|
||||
name: modelData
|
||||
size: 20
|
||||
color: root.currentIcon === modelData ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: iconMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.iconSelected(modelData, "icon")
|
||||
dropdownLoader.active = false
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setIcon(iconName, type) {
|
||||
root.iconType = type
|
||||
root.iconType = "icon"
|
||||
root.currentIcon = iconName
|
||||
}
|
||||
}
|
||||
189
quickshell/Widgets/DankListView.qml
Normal file
189
quickshell/Widgets/DankListView.qml
Normal file
@@ -0,0 +1,189 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Widgets
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
property real mouseWheelSpeed: 60
|
||||
property real savedY: 0
|
||||
property bool justChanged: false
|
||||
property bool isUserScrolling: false
|
||||
property real momentumVelocity: 0
|
||||
property bool isMomentumActive: false
|
||||
property real friction: 0.95
|
||||
property real minMomentumVelocity: 50
|
||||
property real maxMomentumVelocity: 2500
|
||||
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
onMovementStarted: {
|
||||
isUserScrolling = true
|
||||
vbar._scrollBarActive = true
|
||||
vbar.hideTimer.stop()
|
||||
}
|
||||
onMovementEnded: {
|
||||
isUserScrolling = false
|
||||
vbar.hideTimer.restart()
|
||||
}
|
||||
|
||||
onContentYChanged: {
|
||||
if (!justChanged && isUserScrolling) {
|
||||
savedY = contentY
|
||||
}
|
||||
justChanged = false
|
||||
}
|
||||
|
||||
onModelChanged: {
|
||||
justChanged = true
|
||||
contentY = savedY
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
property real touchpadSpeed: 1.8
|
||||
property real lastWheelTime: 0
|
||||
property real momentum: 0
|
||||
property var velocitySamples: []
|
||||
property bool sessionUsedMouseWheel: false
|
||||
|
||||
function startMomentum() {
|
||||
isMomentumActive = true
|
||||
momentumTimer.start()
|
||||
}
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
|
||||
onWheel: event => {
|
||||
isUserScrolling = true
|
||||
vbar._scrollBarActive = true
|
||||
vbar.hideTimer.restart()
|
||||
|
||||
const currentTime = Date.now()
|
||||
const timeDelta = currentTime - lastWheelTime
|
||||
lastWheelTime = currentTime
|
||||
|
||||
const deltaY = event.angleDelta.y
|
||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
|
||||
|
||||
if (isMouseWheel) {
|
||||
sessionUsedMouseWheel = true
|
||||
momentumTimer.stop()
|
||||
isMomentumActive = false
|
||||
velocitySamples = []
|
||||
momentum = 0
|
||||
momentumVelocity = 0
|
||||
|
||||
const lines = Math.floor(Math.abs(deltaY) / 120)
|
||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed
|
||||
let newY = listView.contentY + scrollAmount
|
||||
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY)
|
||||
newY = Math.max(listView.originY, Math.min(maxY, newY))
|
||||
|
||||
if (listView.flicking) {
|
||||
listView.cancelFlick()
|
||||
}
|
||||
|
||||
listView.contentY = newY
|
||||
savedY = newY
|
||||
} else {
|
||||
sessionUsedMouseWheel = false
|
||||
momentumTimer.stop()
|
||||
isMomentumActive = false
|
||||
|
||||
let delta = 0
|
||||
if (event.pixelDelta.y !== 0) {
|
||||
delta = event.pixelDelta.y * touchpadSpeed
|
||||
} else {
|
||||
delta = event.angleDelta.y / 8 * touchpadSpeed
|
||||
}
|
||||
|
||||
velocitySamples.push({
|
||||
"delta": delta,
|
||||
"time": currentTime
|
||||
})
|
||||
velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100)
|
||||
|
||||
if (velocitySamples.length > 1) {
|
||||
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0)
|
||||
const timeSpan = currentTime - velocitySamples[0].time
|
||||
if (timeSpan > 0) {
|
||||
momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000))
|
||||
}
|
||||
}
|
||||
|
||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
||||
momentum = momentum * 0.92 + delta * 0.15
|
||||
delta += momentum
|
||||
} else {
|
||||
momentum = 0
|
||||
}
|
||||
|
||||
let newY = listView.contentY - delta
|
||||
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY)
|
||||
newY = Math.max(listView.originY, Math.min(maxY, newY))
|
||||
|
||||
if (listView.flicking) {
|
||||
listView.cancelFlick()
|
||||
}
|
||||
|
||||
listView.contentY = newY
|
||||
savedY = newY
|
||||
}
|
||||
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
isUserScrolling = false
|
||||
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||
startMomentum()
|
||||
} else {
|
||||
velocitySamples = []
|
||||
momentumVelocity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: momentumTimer
|
||||
interval: 16
|
||||
repeat: true
|
||||
|
||||
onTriggered: {
|
||||
const newY = contentY - momentumVelocity * 0.016
|
||||
const maxY = Math.max(0, contentHeight - height + originY)
|
||||
const minY = originY
|
||||
|
||||
if (newY < minY || newY > maxY) {
|
||||
contentY = newY < minY ? minY : maxY
|
||||
savedY = contentY
|
||||
stop()
|
||||
isMomentumActive = false
|
||||
momentumVelocity = 0
|
||||
return
|
||||
}
|
||||
|
||||
contentY = newY
|
||||
savedY = newY
|
||||
momentumVelocity *= friction
|
||||
|
||||
if (Math.abs(momentumVelocity) < 5) {
|
||||
stop()
|
||||
isMomentumActive = false
|
||||
momentumVelocity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: vbar
|
||||
}
|
||||
}
|
||||
270
quickshell/Widgets/DankLocationSearch.qml
Normal file
270
quickshell/Widgets/DankLocationSearch.qml
Normal file
@@ -0,0 +1,270 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
KeyNavigation.tab: keyNavigationTab
|
||||
KeyNavigation.backtab: keyNavigationBacktab
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
locationInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
property string currentLocation: ""
|
||||
property string placeholderText: I18n.tr("Search for a location...")
|
||||
property bool _internalChange: false
|
||||
property bool isLoading: false
|
||||
property string currentSearchText: ""
|
||||
property Item keyNavigationTab: null
|
||||
property Item keyNavigationBacktab: null
|
||||
|
||||
signal locationSelected(string displayName, string coordinates)
|
||||
|
||||
function resetSearchState() {
|
||||
locationSearchTimer.stop()
|
||||
dropdownHideTimer.stop()
|
||||
isLoading = false
|
||||
searchResultsModel.clear()
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0)
|
||||
|
||||
ListModel {
|
||||
id: searchResultsModel
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: locationSearchTimer
|
||||
|
||||
interval: 500
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (locationInput.text.length > 2) {
|
||||
searchResultsModel.clear()
|
||||
root.isLoading = true
|
||||
const searchLocation = locationInput.text
|
||||
root.currentSearchText = searchLocation
|
||||
const encodedLocation = encodeURIComponent(searchLocation)
|
||||
const curlCommand = `curl -4 -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'`
|
||||
Proc.runCommand("locationSearch", ["bash", "-c", curlCommand], (output, exitCode) => {
|
||||
root.isLoading = false
|
||||
if (exitCode !== 0) {
|
||||
searchResultsModel.clear()
|
||||
return
|
||||
}
|
||||
if (root.currentSearchText !== locationInput.text)
|
||||
return
|
||||
|
||||
const raw = output.trim()
|
||||
searchResultsModel.clear()
|
||||
if (!raw || raw[0] !== "[") {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
if (data.length === 0) {
|
||||
return
|
||||
}
|
||||
for (var i = 0; i < Math.min(data.length, 5); i++) {
|
||||
const location = data[i]
|
||||
if (location.display_name && location.lat && location.lon) {
|
||||
const parts = location.display_name.split(', ')
|
||||
let cleanName = parts[0]
|
||||
if (parts.length > 1) {
|
||||
const state = parts[parts.length - 2]
|
||||
if (state && state !== cleanName)
|
||||
cleanName += `, ${state}`
|
||||
}
|
||||
const query = `${location.lat},${location.lon}`
|
||||
searchResultsModel.append({
|
||||
"name": cleanName,
|
||||
"query": query
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dropdownHideTimer
|
||||
|
||||
interval: 200
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!locationInput.getActiveFocus() && !searchDropdown.hovered)
|
||||
root.resetSearchState()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: searchInputField
|
||||
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
DankTextField {
|
||||
id: locationInput
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
leftIconName: "search"
|
||||
placeholderText: root.placeholderText
|
||||
text: ""
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
focusedBorderColor: Theme.primary
|
||||
keyNavigationTab: root.keyNavigationTab
|
||||
keyNavigationBacktab: root.keyNavigationBacktab
|
||||
onTextEdited: {
|
||||
if (root._internalChange)
|
||||
return
|
||||
if (getActiveFocus()) {
|
||||
if (text.length > 2) {
|
||||
root.isLoading = true
|
||||
locationSearchTimer.restart()
|
||||
} else {
|
||||
root.resetSearchState()
|
||||
}
|
||||
}
|
||||
}
|
||||
onFocusStateChanged: hasFocus => {
|
||||
if (hasFocus) {
|
||||
dropdownHideTimer.stop()
|
||||
} else {
|
||||
dropdownHideTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: root.isLoading ? "hourglass_empty" : (searchResultsModel.count > 0 ? "check_circle" : "error")
|
||||
size: Theme.iconSize - 4
|
||||
color: root.isLoading ? Theme.surfaceVariantText : (searchResultsModel.count > 0 ? Theme.primary : Theme.error)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: (locationInput.getActiveFocus() && locationInput.text.length > 2) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: searchDropdown
|
||||
|
||||
property bool hovered: false
|
||||
|
||||
width: parent.width
|
||||
height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200)
|
||||
y: searchInputField.height
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: Theme.primarySelected
|
||||
border.width: 1
|
||||
visible: locationInput.getActiveFocus() && locationInput.text.length > 2 && (searchResultsModel.count > 0 || root.isLoading)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
parent.hovered = true
|
||||
dropdownHideTimer.stop()
|
||||
}
|
||||
onExited: {
|
||||
parent.hovered = false
|
||||
if (!locationInput.getActiveFocus())
|
||||
dropdownHideTimer.start()
|
||||
}
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
DankListView {
|
||||
id: searchResultsList
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
model: searchResultsModel
|
||||
spacing: 2
|
||||
|
||||
delegate: StyledRect {
|
||||
width: searchResultsList.width
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: resultMouseArea.containsMouse ? Theme.surfaceLight : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "place"
|
||||
size: Theme.iconSize - 6
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: model.name || "Unknown"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 30
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: resultMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root._internalChange = true
|
||||
const selectedName = model.name
|
||||
const selectedQuery = model.query
|
||||
locationInput.text = selectedName
|
||||
root.locationSelected(selectedName, selectedQuery)
|
||||
root.resetSearchState()
|
||||
locationInput.setFocus(false)
|
||||
root._internalChange = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: root.isLoading ? "Searching..." : "No locations found"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: searchResultsList.count === 0 && locationInput.text.length > 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
quickshell/Widgets/DankNFIcon.qml
Normal file
153
quickshell/Widgets/DankNFIcon.qml
Normal file
@@ -0,0 +1,153 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string name: ""
|
||||
property int size: Theme.fontSizeMedium
|
||||
property alias color: icon.color
|
||||
|
||||
width: size
|
||||
height: size
|
||||
visible: text.length > 0
|
||||
|
||||
// This is for file browser, particularly - might want another map later for app IDs
|
||||
readonly property var iconMap: ({
|
||||
// --- Distribution logos ---
|
||||
"debian": "\u{f08da}",
|
||||
"arch": "\u{f08c7}",
|
||||
"archcraft": "\u{f345}",
|
||||
"guix": "\u{f325}",
|
||||
"fedora": "\u{f08db}",
|
||||
"nixos": "\u{f1105}",
|
||||
"ubuntu": "\u{f0548}",
|
||||
"gentoo": "\u{f08e8}",
|
||||
"endeavouros": "\u{f322}",
|
||||
"manjaro": "\u{f160a}",
|
||||
"opensuse": "\u{f314}",
|
||||
|
||||
// --- special types ---
|
||||
"folder": "\u{F024B}",
|
||||
"file": "\u{F0214}",
|
||||
|
||||
// --- special filenames (no extension) ---
|
||||
"docker": "\u{F0868}",
|
||||
"makefile": "\u{F09EE}",
|
||||
"license": "\u{F09EE}",
|
||||
"readme": "\u{F0354}",
|
||||
|
||||
// --- programming languages ---
|
||||
"rs": "\u{F1617}",
|
||||
"dart": "\u{e798}",
|
||||
"go": "\u{F07D3}",
|
||||
"py": "\u{F0320}",
|
||||
"js": "\u{F031E}",
|
||||
"jsx": "\u{F031E}",
|
||||
"ts": "\u{F06E6}",
|
||||
"tsx": "\u{F06E6}",
|
||||
"java": "\u{F0B37}",
|
||||
"c": "\u{F0671}",
|
||||
"cpp": "\u{F0672}",
|
||||
"cxx": "\u{F0672}",
|
||||
"h": "\u{F0672}",
|
||||
"hpp": "\u{F0672}",
|
||||
"cs": "\u{F031B}",
|
||||
"html": "\u{e60e}",
|
||||
"htm": "\u{e60e}",
|
||||
"css": "\u{E6b8}",
|
||||
"scss": "\u{F031C}",
|
||||
"less": "\u{F031C}",
|
||||
"md": "\u{F0354}",
|
||||
"markdown": "\u{F0354}",
|
||||
"json": "\u{eb0f}",
|
||||
"jsonc": "\u{eb0f}",
|
||||
"yaml": "\u{e8eb}",
|
||||
"yml": "\u{e8eb}",
|
||||
"xml": "\u{F09EE}",
|
||||
"sql": "\u{f1c0}",
|
||||
|
||||
// --- scripts / shells ---
|
||||
"sh": "\u{f0bc1}",
|
||||
"bash": "\u{f0bc1}",
|
||||
"zsh": "\u{f0bc1}",
|
||||
"fish": "\u{f0bc1}",
|
||||
"ps1": "\u{f0bc1}",
|
||||
"bat": "\u{f0bc1}",
|
||||
|
||||
// --- data / config ---
|
||||
"toml": "\u{e6b2}",
|
||||
"ini": "\u{F09EE}",
|
||||
"conf": "\u{F09EE}",
|
||||
"cfg": "\u{F09EE}",
|
||||
"csv": "\u{eefc}",
|
||||
"tsv": "\u{F021C}",
|
||||
|
||||
// --- docs / office ---
|
||||
"pdf": "\u{F0226}",
|
||||
"doc": "\u{F09EE}",
|
||||
"docx": "\u{F09EE}",
|
||||
"rtf": "\u{F09EE}",
|
||||
"ppt": "\u{F09EE}",
|
||||
"pptx": "\u{F09EE}",
|
||||
"log": "\u{F09EE}",
|
||||
"xls": "\u{F021C}",
|
||||
"xlsx": "\u{F021C}",
|
||||
|
||||
// --- images ---
|
||||
"ico": "\u{F021F}",
|
||||
|
||||
// --- audio / video ---
|
||||
"mp3": "\u{e638}",
|
||||
"wav": "\u{e638}",
|
||||
"flac": "\u{e638}",
|
||||
"ogg": "\u{e638}",
|
||||
"mp4": "\u{f0567}",
|
||||
"mkv": "\u{f0567}",
|
||||
"webm": "\u{f0567}",
|
||||
"mov": "\u{f0567}",
|
||||
|
||||
// --- archives / packages ---
|
||||
"zip": "\u{e6aa}",
|
||||
"tar": "\u{f003c}",
|
||||
"gz": "\u{f003c}",
|
||||
"bz2": "\u{f003c}",
|
||||
"7z": "\u{f003c}",
|
||||
|
||||
// --- containers / infra / cloud ---
|
||||
"dockerfile": "\u{F0868}",
|
||||
"yml.k8s": "\u{F09EE}",
|
||||
"yaml.k8s": "\u{F09EE}",
|
||||
"tf": "\u{F09EE}",
|
||||
"tfvars": "\u{F09EE}"
|
||||
})
|
||||
|
||||
|
||||
readonly property string text: iconMap[name] || iconMap["file"] || ""
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase()
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
return "docker"
|
||||
}
|
||||
const ext = fileName.split('.').pop()
|
||||
return ext || ""
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: firaCodeFont
|
||||
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
font.family: firaCodeFont.name
|
||||
font.pixelSize: root.size
|
||||
color: Theme.surfaceText
|
||||
text: root.text
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
191
quickshell/Widgets/DankOSD.qml
Normal file
191
quickshell/Widgets/DankOSD.qml
Normal file
@@ -0,0 +1,191 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property string blurNamespace: "dms:osd"
|
||||
WlrLayershell.namespace: blurNamespace
|
||||
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property var modelData
|
||||
property bool shouldBeVisible: false
|
||||
property int autoHideInterval: 2000
|
||||
property bool enableMouseInteraction: false
|
||||
property real osdWidth: Theme.iconSize + Theme.spacingS * 2
|
||||
property real osdHeight: Theme.iconSize + Theme.spacingS * 2
|
||||
property int animationDuration: Theme.mediumDuration
|
||||
property var animationEasing: Theme.emphasizedEasing
|
||||
|
||||
signal osdShown
|
||||
signal osdHidden
|
||||
|
||||
function show() {
|
||||
closeTimer.stop()
|
||||
shouldBeVisible = true
|
||||
visible = true
|
||||
hideTimer.restart()
|
||||
osdShown()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
shouldBeVisible = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
function resetHideTimer() {
|
||||
if (shouldBeVisible) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function updateHoverState() {
|
||||
let isHovered = (enableMouseInteraction && mouseArea.containsMouse) || osdContainer.childHovered
|
||||
if (enableMouseInteraction) {
|
||||
if (isHovered) {
|
||||
hideTimer.stop()
|
||||
} else if (shouldBeVisible) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setChildHovered(hovered) {
|
||||
osdContainer.childHovered = hovered
|
||||
updateHoverState()
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
visible: false
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
||||
readonly property real screenWidth: screen.width
|
||||
readonly property real screenHeight: screen.height
|
||||
readonly property real alignedWidth: Theme.px(osdWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(osdHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap((screenWidth - alignedWidth) / 2, dpr)
|
||||
readonly property real alignedY: Theme.snap(screenHeight - alignedHeight - Theme.spacingM, dpr)
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
|
||||
interval: autoHideInterval
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!enableMouseInteraction || !mouseArea.containsMouse) {
|
||||
hide()
|
||||
} else {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration + 50
|
||||
onTriggered: {
|
||||
if (!shouldBeVisible) {
|
||||
visible = false
|
||||
osdHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: osdContainer
|
||||
x: alignedX
|
||||
y: alignedY
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
opacity: shouldBeVisible ? 1 : 0
|
||||
scale: shouldBeVisible ? 1 : 0.9
|
||||
|
||||
property bool childHovered: false
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha * osdContainer.opacity))
|
||||
|
||||
Item {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
visible: osdContainer.popupSurfaceAlpha >= 0.95
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
id: shadowFx
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
property int blurMax: 64
|
||||
shadowBlur: Math.max(0, Math.min(1, osdContainer.shadowBlurPx / blurMax))
|
||||
shadowScale: 1 + (2 * osdContainer.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height))
|
||||
shadowColor: Qt.rgba(0, 0, 0, osdContainer.effectiveShadowAlpha)
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: enableMouseInteraction
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
z: -1
|
||||
onContainsMouseChanged: updateHoverState()
|
||||
}
|
||||
|
||||
onChildHoveredChanged: updateHoverState()
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.visible
|
||||
asynchronous: false
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: animationEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: animationEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: bgShadowLayer
|
||||
}
|
||||
}
|
||||
265
quickshell/Widgets/DankPopout.qml
Normal file
265
quickshell/Widgets/DankPopout.qml
Normal file
@@ -0,0 +1,265 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property string layerNamespace: "dms:popout"
|
||||
WlrLayershell.namespace: layerNamespace
|
||||
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property real popupWidth: 400
|
||||
property real popupHeight: 300
|
||||
property real triggerX: 0
|
||||
property real triggerY: 0
|
||||
property real triggerWidth: 40
|
||||
property string triggerSection: ""
|
||||
property string positioning: "center"
|
||||
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property bool shouldBeVisible: false
|
||||
property int keyboardFocusMode: WlrKeyboardFocus.OnDemand
|
||||
|
||||
signal opened
|
||||
signal popoutClosed
|
||||
signal backgroundClicked
|
||||
|
||||
function open() {
|
||||
closeTimer.stop()
|
||||
shouldBeVisible = true
|
||||
visible = true
|
||||
opened()
|
||||
}
|
||||
|
||||
function close() {
|
||||
shouldBeVisible = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible)
|
||||
close()
|
||||
else
|
||||
open()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration
|
||||
onTriggered: {
|
||||
if (!shouldBeVisible) {
|
||||
visible = false
|
||||
popoutClosed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
|
||||
case "bottom":
|
||||
return WlrLayershell.Bottom
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay
|
||||
case "background":
|
||||
return WlrLayershell.Background
|
||||
default:
|
||||
return WlrLayershell.Top
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: shouldBeVisible ? keyboardFocusMode : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
readonly property real screenWidth: root.screen.width
|
||||
readonly property real screenHeight: root.screen.height
|
||||
readonly property real dpr: CompositorService.getScreenScale(root.screen)
|
||||
|
||||
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap((() => {
|
||||
if (SettingsData.dankBarPosition === SettingsData.Position.Left) {
|
||||
return triggerY + SettingsData.dankBarBottomGap
|
||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Right) {
|
||||
return screenWidth - triggerY - SettingsData.dankBarBottomGap - popupWidth
|
||||
} else {
|
||||
const centerX = triggerX + (triggerWidth / 2) - (popupWidth / 2)
|
||||
return Math.max(Theme.popupDistance, Math.min(screenWidth - popupWidth - Theme.popupDistance, centerX))
|
||||
}
|
||||
})(), dpr)
|
||||
readonly property real alignedY: Theme.snap((() => {
|
||||
if (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right) {
|
||||
const centerY = triggerX + (triggerWidth / 2) - (popupHeight / 2)
|
||||
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, centerY))
|
||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Bottom) {
|
||||
return Math.max(Theme.popupDistance, screenHeight - triggerY - popupHeight)
|
||||
} else {
|
||||
return Math.min(screenHeight - popupHeight - Theme.popupDistance, triggerY)
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: shouldBeVisible && contentLoader.opacity > 0.1
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.x < alignedX || mouse.x > alignedX + alignedWidth ||
|
||||
mouse.y < alignedY || mouse.y > alignedY + alignedHeight) {
|
||||
backgroundClicked()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
x: alignedX
|
||||
y: alignedY
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
|
||||
readonly property bool barTop: SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
readonly property bool barBottom: SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
readonly property bool barLeft: SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
readonly property bool barRight: SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
|
||||
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr)
|
||||
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr)
|
||||
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animX {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: shouldBeVisible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha * contentWrapper.opacity))
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
visible: contentWrapper.popupSurfaceAlpha >= 0.95
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
id: shadowFx
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
property int blurMax: 64
|
||||
shadowBlur: Math.max(0, Math.min(1, contentWrapper.shadowBlurPx / blurMax))
|
||||
shadowScale: 1 + (2 * contentWrapper.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height))
|
||||
shadowColor: Qt.rgba(0, 0, 0, contentWrapper.effectiveShadowAlpha)
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentLoaderWrapper
|
||||
anchors.fill: parent
|
||||
x: Theme.snap(x, root.dpr)
|
||||
y: Theme.snap(y, root.dpr)
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.visible
|
||||
asynchronous: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
parent: contentContainer
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
onVisibleChanged: if (visible) forceActiveFocus()
|
||||
}
|
||||
}
|
||||
54
quickshell/Widgets/DankRectangle.qml
Normal file
54
quickshell/Widgets/DankRectangle.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: Theme.surfaceContainer
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 1
|
||||
property real radius: Theme.cornerRadius
|
||||
property color overlayColor: "transparent"
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.color
|
||||
strokeColor: root.borderColor
|
||||
strokeWidth: root.borderWidth
|
||||
|
||||
startX: root.radius
|
||||
startY: 0
|
||||
|
||||
PathLine { x: root.width - root.radius; y: 0 }
|
||||
PathQuad { x: root.width; y: root.radius; controlX: root.width; controlY: 0 }
|
||||
PathLine { x: root.width; y: root.height - root.radius }
|
||||
PathQuad { x: root.width - root.radius; y: root.height; controlX: root.width; controlY: root.height }
|
||||
PathLine { x: root.radius; y: root.height }
|
||||
PathQuad { x: 0; y: root.height - root.radius; controlX: 0; controlY: root.height }
|
||||
PathLine { x: 0; y: root.radius }
|
||||
PathQuad { x: root.radius; y: 0; controlX: 0; controlY: 0 }
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.overlayColor
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
|
||||
startX: root.radius
|
||||
startY: 0
|
||||
|
||||
PathLine { x: root.width - root.radius; y: 0 }
|
||||
PathQuad { x: root.width; y: root.radius; controlX: root.width; controlY: 0 }
|
||||
PathLine { x: root.width; y: root.height - root.radius }
|
||||
PathQuad { x: root.width - root.radius; y: root.height; controlX: root.width; controlY: root.height }
|
||||
PathLine { x: root.radius; y: root.height }
|
||||
PathQuad { x: 0; y: root.height - root.radius; controlX: 0; controlY: root.height }
|
||||
PathLine { x: 0; y: root.radius }
|
||||
PathQuad { x: root.radius; y: 0; controlX: 0; controlY: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
43
quickshell/Widgets/DankScrollbar.qml
Normal file
43
quickshell/Widgets/DankScrollbar.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
|
||||
ScrollBar {
|
||||
id: scrollbar
|
||||
|
||||
property bool _scrollBarActive: false
|
||||
property alias hideTimer: hideScrollBarTimer
|
||||
property bool _isParentMoving: parent && (parent.moving || parent.flicking || parent.isMomentumActive)
|
||||
property bool _shouldShow: pressed || hovered || active || _isParentMoving || _scrollBarActive
|
||||
|
||||
policy: (parent && parent.contentHeight > parent.height) ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
|
||||
minimumSize: 0.08
|
||||
implicitWidth: 8
|
||||
interactive: true
|
||||
hoverEnabled: true
|
||||
z: 1000
|
||||
opacity: (policy !== ScrollBar.AlwaysOff && _shouldShow) ? 1.0 : 0.0
|
||||
visible: policy !== ScrollBar.AlwaysOff
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 160
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 6
|
||||
radius: width / 2
|
||||
color: scrollbar.pressed ? Theme.primary : scrollbar._shouldShow ? Theme.outline : Theme.outlineMedium
|
||||
opacity: scrollbar.pressed ? 1.0 : scrollbar._shouldShow ? 1.0 : 0.6
|
||||
}
|
||||
|
||||
background: Item {}
|
||||
|
||||
Timer {
|
||||
id: hideScrollBarTimer
|
||||
interval: 1200
|
||||
onTriggered: scrollbar._scrollBarActive = false
|
||||
}
|
||||
}
|
||||
182
quickshell/Widgets/DankSeekbar.qml
Normal file
182
quickshell/Widgets/DankSeekbar.qml
Normal file
@@ -0,0 +1,182 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property MprisPlayer activePlayer
|
||||
property real value: {
|
||||
if (!activePlayer || activePlayer.length <= 0) return 0
|
||||
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length)
|
||||
const calculatedRatio = pos / activePlayer.length
|
||||
return Math.max(0, Math.min(1, calculatedRatio))
|
||||
}
|
||||
property bool isSeeking: false
|
||||
|
||||
implicitHeight: 20
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
visible: activePlayer && activePlayer.length > 0
|
||||
sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent
|
||||
z: 1
|
||||
|
||||
Component {
|
||||
id: waveProgressComponent
|
||||
|
||||
M3WaveProgress {
|
||||
value: root.value
|
||||
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
|
||||
|
||||
property real pendingSeekPosition: -1
|
||||
|
||||
Timer {
|
||||
id: waveSeekDebounceTimer
|
||||
interval: 150
|
||||
onTriggered: {
|
||||
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
parent.pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: (mouse) => {
|
||||
root.isSeeking = true
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
waveSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
root.isSeeking = false
|
||||
waveSeekDebounceTimer.stop()
|
||||
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
waveSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
activePlayer.position = r * activePlayer.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: flatProgressComponent
|
||||
|
||||
Item {
|
||||
property real lineWidth: 3
|
||||
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
|
||||
property color fillColor: Theme.primary
|
||||
property color playheadColor: Theme.primary
|
||||
readonly property real midY: height / 2
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.lineWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: parent.trackColor
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(0, Math.min(parent.width, parent.width * root.value))
|
||||
height: parent.lineWidth
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: parent.fillColor
|
||||
radius: height / 2
|
||||
Behavior on width { NumberAnimation { duration: 80 } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: playhead
|
||||
width: 3
|
||||
height: Math.max(parent.lineWidth + 8, 14)
|
||||
radius: width / 2
|
||||
color: parent.playheadColor
|
||||
x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2
|
||||
y: parent.midY - height / 2
|
||||
z: 3
|
||||
Behavior on x { NumberAnimation { duration: 80 } }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
|
||||
|
||||
property real pendingSeekPosition: -1
|
||||
|
||||
Timer {
|
||||
id: flatSeekDebounceTimer
|
||||
interval: 150
|
||||
onTriggered: {
|
||||
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
parent.pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: (mouse) => {
|
||||
root.isSeeking = true
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
flatSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
root.isSeeking = false
|
||||
flatSeekDebounceTimer.stop()
|
||||
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
flatSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
activePlayer.position = r * activePlayer.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
204
quickshell/Widgets/DankSlideout.qml
Normal file
204
quickshell/Widgets/DankSlideout.qml
Normal file
@@ -0,0 +1,204 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property string layerNamespace: "dms:slideout"
|
||||
WlrLayershell.namespace: layerNamespace
|
||||
|
||||
property bool isVisible: false
|
||||
property var targetScreen: null
|
||||
property var modelData: null
|
||||
property real slideoutWidth: 480
|
||||
property bool expandable: false
|
||||
property bool expandedWidth: false
|
||||
property real expandedWidthValue: 960
|
||||
property Component content: null
|
||||
property string title: ""
|
||||
property alias container: contentContainer
|
||||
property real customTransparency: -1
|
||||
|
||||
function show() {
|
||||
visible = true
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isVisible) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
visible: isVisible
|
||||
screen: modelData
|
||||
|
||||
anchors.top: true
|
||||
anchors.bottom: true
|
||||
anchors.right: true
|
||||
|
||||
implicitWidth: expandable ? expandedWidthValue : slideoutWidth
|
||||
implicitHeight: modelData ? modelData.height : 800
|
||||
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: 0
|
||||
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
|
||||
readonly property real dpr: CompositorService.getScreenScale(root.screen)
|
||||
readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modelData ? modelData.height : 800, dpr)
|
||||
|
||||
mask: Region {
|
||||
item: Rectangle {
|
||||
x: root.width - alignedWidth
|
||||
y: 0
|
||||
width: alignedWidth
|
||||
height: root.height
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: slideContainer
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
|
||||
property real slideOffset: alignedWidth
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onIsVisibleChanged() {
|
||||
slideContainer.slideOffset = root.isVisible ? 0 : slideContainer.width
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on slideOffset {
|
||||
NumberAnimation {
|
||||
id: slideAnimation
|
||||
duration: 450
|
||||
easing.type: Easing.OutCubic
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running && !isVisible) {
|
||||
root.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 250
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentRect
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
x: Theme.snap(slideContainer.slideOffset, root.dpr)
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b,
|
||||
customTransparency >= 0 ? customTransparency : SettingsData.popupTransparency)
|
||||
}
|
||||
|
||||
Column {
|
||||
id: headerColumn
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
visible: root.title !== ""
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
Column {
|
||||
width: parent.width - buttonRow.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonRow
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
id: expandButton
|
||||
iconName: root.expandedWidth ? "unfold_less" : "unfold_more"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
visible: root.expandable
|
||||
onClicked: root.expandedWidth = !root.expandedWidth
|
||||
|
||||
transform: Rotation {
|
||||
angle: 90
|
||||
origin.x: expandButton.width / 2
|
||||
origin.y: expandButton.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: closeButton
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.top: root.title !== "" ? headerColumn.bottom : parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: root.title !== "" ? 0 : Theme.spacingL
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
274
quickshell/Widgets/DankSlider.qml
Normal file
274
quickshell/Widgets/DankSlider.qml
Normal file
@@ -0,0 +1,274 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: slider
|
||||
|
||||
property int value: 50
|
||||
property int minimum: 0
|
||||
property int maximum: 100
|
||||
property string leftIcon: ""
|
||||
property string rightIcon: ""
|
||||
property bool enabled: true
|
||||
property string unit: "%"
|
||||
property bool showValue: true
|
||||
property bool isDragging: false
|
||||
property bool wheelEnabled: true
|
||||
property real valueOverride: -1
|
||||
property bool alwaysShowValue: false
|
||||
readonly property bool containsMouse: sliderMouseArea.containsMouse
|
||||
|
||||
property color thumbOutlineColor: Theme.surfaceContainer
|
||||
property color trackColor: enabled ? Theme.outline : Theme.outline
|
||||
|
||||
signal sliderValueChanged(int newValue)
|
||||
signal sliderDragFinished(int finalValue)
|
||||
|
||||
height: 48
|
||||
|
||||
function updateValueFromPosition(x) {
|
||||
let ratio = Math.max(0, Math.min(1, (x - sliderHandle.width / 2) / (sliderTrack.width - sliderHandle.width)))
|
||||
let newValue = Math.round(minimum + ratio * (maximum - minimum))
|
||||
if (newValue !== value) {
|
||||
value = newValue
|
||||
sliderValueChanged(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: slider.leftIcon
|
||||
size: Theme.iconSize
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.leftIcon.length > 0
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: sliderTrack
|
||||
|
||||
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
|
||||
property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
|
||||
|
||||
width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
|
||||
height: 12
|
||||
radius: Theme.cornerRadius
|
||||
color: slider.trackColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
clip: false
|
||||
|
||||
StyledRect {
|
||||
id: sliderFill
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
width: {
|
||||
const ratio = (slider.value - slider.minimum) / (slider.maximum - slider.minimum)
|
||||
const travel = sliderTrack.width - sliderHandle.width
|
||||
const center = (travel * ratio) + sliderHandle.width / 2
|
||||
return Math.max(0, Math.min(sliderTrack.width, center))
|
||||
}
|
||||
color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: sliderHandle
|
||||
|
||||
property bool active: sliderMouseArea.containsMouse || sliderMouseArea.pressed || slider.isDragging
|
||||
|
||||
width: 8
|
||||
height: 24
|
||||
radius: Theme.cornerRadius
|
||||
x: {
|
||||
const ratio = (slider.value - slider.minimum) / (slider.maximum - slider.minimum)
|
||||
const travel = sliderTrack.width - width
|
||||
return Math.max(0, Math.min(travel, travel * ratio))
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
|
||||
border.width: 3
|
||||
border.color: slider.thumbOutlineColor
|
||||
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.onPrimary
|
||||
opacity: slider.enabled ? (sliderMouseArea.pressed ? 0.16 : (sliderMouseArea.containsMouse ? 0.08 : 0)) : 0
|
||||
visible: opacity > 0
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 20
|
||||
height: parent.height + 20
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.width: 2
|
||||
border.color: Theme.primary
|
||||
opacity: slider.enabled && slider.focus ? 0.3 : 0
|
||||
visible: opacity > 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: ripple
|
||||
anchors.centerIn: parent
|
||||
width: 0
|
||||
height: 0
|
||||
radius: width / 2
|
||||
color: Theme.onPrimary
|
||||
opacity: 0
|
||||
|
||||
function start() {
|
||||
opacity = 0.16
|
||||
width = 0
|
||||
height = 0
|
||||
rippleAnimation.start()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: rippleAnimation
|
||||
NumberAnimation {
|
||||
target: ripple
|
||||
properties: "width,height"
|
||||
to: 28
|
||||
duration: 180
|
||||
}
|
||||
NumberAnimation {
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onPressedChanged: {
|
||||
if (pressed && slider.enabled) {
|
||||
ripple.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
scale: active ? 1.05 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: sliderContainer
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
MouseArea {
|
||||
id: sliderMouseArea
|
||||
|
||||
property bool isDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: -10
|
||||
anchors.bottomMargin: -10
|
||||
hoverEnabled: true
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: slider.enabled
|
||||
preventStealing: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onWheel: wheelEvent => {
|
||||
if (!slider.wheelEnabled) {
|
||||
wheelEvent.accepted = false
|
||||
return
|
||||
}
|
||||
let step = Math.max(0.5, (maximum - minimum) / 100)
|
||||
let newValue = wheelEvent.angleDelta.y > 0 ? Math.min(maximum, value + step) : Math.max(minimum, value - step)
|
||||
newValue = Math.round(newValue)
|
||||
if (newValue !== value) {
|
||||
value = newValue
|
||||
sliderValueChanged(newValue)
|
||||
}
|
||||
wheelEvent.accepted = true
|
||||
}
|
||||
onPressed: mouse => {
|
||||
if (slider.enabled) {
|
||||
slider.isDragging = true
|
||||
sliderMouseArea.isDragging = true
|
||||
updateValueFromPosition(mouse.x)
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
if (slider.enabled) {
|
||||
slider.isDragging = false
|
||||
sliderMouseArea.isDragging = false
|
||||
slider.sliderDragFinished(slider.value)
|
||||
}
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed && slider.isDragging && slider.enabled) {
|
||||
updateValueFromPosition(mouse.x)
|
||||
}
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (slider.enabled && !slider.isDragging) {
|
||||
updateValueFromPosition(mouse.x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: valueTooltip
|
||||
|
||||
width: tooltipText.contentWidth + Theme.spacingS * 2
|
||||
height: tooltipText.contentHeight + Theme.spacingXS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
x: Math.max(0, Math.min(parent.width - width, sliderHandle.x + sliderHandle.width/2 - width/2))
|
||||
visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue))
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
StyledText {
|
||||
id: tooltipText
|
||||
|
||||
text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
font.hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: slider.rightIcon
|
||||
size: Theme.iconSize
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.rightIcon.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
254
quickshell/Widgets/DankTabBar.qml
Normal file
254
quickshell/Widgets/DankTabBar.qml
Normal file
@@ -0,0 +1,254 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
FocusScope {
|
||||
id: tabBar
|
||||
|
||||
property alias model: tabRepeater.model
|
||||
property int currentIndex: 0
|
||||
property int spacing: Theme.spacingL
|
||||
property int tabHeight: 56
|
||||
property bool showIcons: true
|
||||
property bool equalWidthTabs: true
|
||||
property bool enableArrowNavigation: true
|
||||
property Item nextFocusTarget: null
|
||||
property Item previousFocusTarget: null
|
||||
|
||||
signal tabClicked(int index)
|
||||
signal actionTriggered(int index)
|
||||
|
||||
focus: false
|
||||
activeFocusOnTab: true
|
||||
height: tabHeight
|
||||
|
||||
KeyNavigation.tab: nextFocusTarget
|
||||
KeyNavigation.down: nextFocusTarget
|
||||
KeyNavigation.backtab: previousFocusTarget
|
||||
KeyNavigation.up: previousFocusTarget
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (!tabBar.activeFocus || tabRepeater.count === 0)
|
||||
return
|
||||
|
||||
function findSelectableIndex(startIndex, step) {
|
||||
let idx = startIndex
|
||||
for (let i = 0; i < tabRepeater.count; i++) {
|
||||
idx = (idx + step + tabRepeater.count) % tabRepeater.count
|
||||
const item = tabRepeater.itemAt(idx)
|
||||
if (item && !item.isAction)
|
||||
return idx
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
const goToIndex = (nextIndex) => {
|
||||
if (nextIndex >= 0 && nextIndex !== tabBar.currentIndex) {
|
||||
tabBar.currentIndex = nextIndex
|
||||
tabBar.tabClicked(nextIndex)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveTarget = (item) => {
|
||||
if (!item)
|
||||
return null
|
||||
|
||||
if (item.focusTarget)
|
||||
return resolveTarget(item.focusTarget)
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
const focusItem = (item) => {
|
||||
const target = resolveTarget(item)
|
||||
if (!target)
|
||||
return false
|
||||
|
||||
if (target.requestFocus) {
|
||||
Qt.callLater(() => target.requestFocus())
|
||||
return true
|
||||
}
|
||||
|
||||
if (target.forceActiveFocus) {
|
||||
Qt.callLater(() => target.forceActiveFocus())
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Right && tabBar.enableArrowNavigation) {
|
||||
const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : -1
|
||||
const nextIndex = findSelectableIndex(baseIndex, 1)
|
||||
if (nextIndex >= 0) {
|
||||
goToIndex(nextIndex)
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Left && tabBar.enableArrowNavigation) {
|
||||
const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : 0
|
||||
const nextIndex = findSelectableIndex(baseIndex, -1)
|
||||
if (nextIndex >= 0) {
|
||||
goToIndex(nextIndex)
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier)) {
|
||||
if (focusItem(tabBar.previousFocusTarget)) {
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down) {
|
||||
if (focusItem(tabBar.nextFocusTarget)) {
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
if (focusItem(tabBar.previousFocusTarget)) {
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: tabRow
|
||||
anchors.fill: parent
|
||||
spacing: tabBar.spacing
|
||||
|
||||
Repeater {
|
||||
id: tabRepeater
|
||||
|
||||
Item {
|
||||
id: tabItem
|
||||
property bool isAction: modelData && modelData.isAction === true
|
||||
property bool isActive: !isAction && tabBar.currentIndex === index
|
||||
property bool hasIcon: tabBar.showIcons && modelData && modelData.icon && modelData.icon.length > 0
|
||||
property bool hasText: modelData && modelData.text && modelData.text.length > 0
|
||||
|
||||
width: tabBar.equalWidthTabs ? (tabBar.width - tabBar.spacing * Math.max(0, tabRepeater.count - 1)) / Math.max(1, tabRepeater.count) : Math.max(contentCol.implicitWidth + Theme.spacingXL, 64)
|
||||
height: tabBar.tabHeight
|
||||
|
||||
Column {
|
||||
id: contentCol
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon || ""
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
size: Theme.iconSize
|
||||
color: tabItem.isActive ? Theme.primary : Theme.surfaceText
|
||||
visible: hasIcon
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.text || ""
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: tabItem.isActive ? Theme.primary : Theme.surfaceText
|
||||
font.weight: tabItem.isActive ? Font.Medium : Font.Normal
|
||||
visible: hasText
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stateLayer
|
||||
anchors.fill: parent
|
||||
color: Theme.surfaceTint
|
||||
opacity: tabArea.pressed ? 0.12 : (tabArea.containsMouse ? 0.08 : 0)
|
||||
visible: opacity > 0
|
||||
radius: Theme.cornerRadius
|
||||
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (tabItem.isAction) {
|
||||
tabBar.actionTriggered(index)
|
||||
} else {
|
||||
tabBar.tabClicked(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
y: parent.height + 7
|
||||
height: 3
|
||||
width: 60
|
||||
topLeftRadius: Theme.cornerRadius
|
||||
topRightRadius: Theme.cornerRadius
|
||||
bottomLeftRadius: 0
|
||||
bottomRightRadius: 0
|
||||
color: Theme.primary
|
||||
visible: false
|
||||
|
||||
property bool animationEnabled: false
|
||||
property bool initialSetupComplete: false
|
||||
|
||||
Behavior on x {
|
||||
enabled: indicator.animationEnabled
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: indicator.animationEnabled
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
y: parent.height + 10
|
||||
color: Theme.outlineStrong
|
||||
}
|
||||
|
||||
function updateIndicator() {
|
||||
if (tabRepeater.count === 0 || currentIndex < 0 || currentIndex >= tabRepeater.count) {
|
||||
return
|
||||
}
|
||||
|
||||
const item = tabRepeater.itemAt(currentIndex)
|
||||
if (!item || item.isAction) {
|
||||
return
|
||||
}
|
||||
|
||||
const tabPos = item.mapToItem(tabBar, 0, 0)
|
||||
const tabCenterX = tabPos.x + item.width / 2
|
||||
const indicatorWidth = 60
|
||||
|
||||
if (tabPos.x < 10 && currentIndex > 0) {
|
||||
Qt.callLater(updateIndicator)
|
||||
return
|
||||
}
|
||||
|
||||
if (!indicator.initialSetupComplete) {
|
||||
indicator.animationEnabled = false
|
||||
indicator.width = indicatorWidth
|
||||
indicator.x = tabCenterX - indicatorWidth / 2
|
||||
indicator.visible = true
|
||||
indicator.initialSetupComplete = true
|
||||
indicator.animationEnabled = true
|
||||
} else {
|
||||
indicator.width = indicatorWidth
|
||||
indicator.x = tabCenterX - indicatorWidth / 2
|
||||
indicator.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
Qt.callLater(updateIndicator)
|
||||
}
|
||||
onWidthChanged: Qt.callLater(updateIndicator)
|
||||
}
|
||||
204
quickshell/Widgets/DankTextField.qml
Normal file
204
quickshell/Widgets/DankTextField.qml
Normal file
@@ -0,0 +1,204 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
KeyNavigation.tab: keyNavigationTab
|
||||
KeyNavigation.backtab: keyNavigationBacktab
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
textInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
property alias text: textInput.text
|
||||
property string placeholderText: ""
|
||||
property alias font: textInput.font
|
||||
property alias textColor: textInput.color
|
||||
property alias enabled: textInput.enabled
|
||||
property alias echoMode: textInput.echoMode
|
||||
property alias validator: textInput.validator
|
||||
property alias maximumLength: textInput.maximumLength
|
||||
property string leftIconName: ""
|
||||
property int leftIconSize: Theme.iconSize
|
||||
property color leftIconColor: Theme.surfaceVariantText
|
||||
property color leftIconFocusedColor: Theme.primary
|
||||
property bool showClearButton: false
|
||||
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
property color focusedBorderColor: Theme.primary
|
||||
property color normalBorderColor: Theme.outlineMedium
|
||||
property color placeholderColor: Theme.outlineButton
|
||||
property int borderWidth: 1
|
||||
property int focusedBorderWidth: 2
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0)
|
||||
readonly property real rightPadding: Theme.spacingM + (showClearButton && text.length > 0 ? 24 + Theme.spacingM : 0)
|
||||
property real topPadding: Theme.spacingM
|
||||
property real bottomPadding: Theme.spacingM
|
||||
property bool ignoreLeftRightKeys: false
|
||||
property bool ignoreTabKeys: false
|
||||
property var keyForwardTargets: []
|
||||
property Item keyNavigationTab: null
|
||||
property Item keyNavigationBacktab: null
|
||||
|
||||
signal textEdited
|
||||
signal editingFinished
|
||||
signal accepted
|
||||
signal focusStateChanged(bool hasFocus)
|
||||
|
||||
function getActiveFocus() {
|
||||
return textInput.activeFocus
|
||||
}
|
||||
function setFocus(value) {
|
||||
textInput.focus = value
|
||||
}
|
||||
function forceActiveFocus() {
|
||||
textInput.forceActiveFocus()
|
||||
}
|
||||
function selectAll() {
|
||||
textInput.selectAll()
|
||||
}
|
||||
function clear() {
|
||||
textInput.clear()
|
||||
}
|
||||
function insertText(str) {
|
||||
textInput.insert(textInput.cursorPosition, str)
|
||||
}
|
||||
|
||||
width: 200
|
||||
height: 48
|
||||
radius: cornerRadius
|
||||
color: backgroundColor
|
||||
border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
|
||||
border.width: textInput.activeFocus ? focusedBorderWidth : borderWidth
|
||||
|
||||
DankIcon {
|
||||
id: leftIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: leftIconName
|
||||
size: leftIconSize
|
||||
color: textInput.activeFocus ? leftIconFocusedColor : leftIconColor
|
||||
visible: leftIconName !== ""
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: textInput
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.leftPadding
|
||||
anchors.rightMargin: root.rightPadding
|
||||
anchors.topMargin: root.topPadding
|
||||
anchors.bottomMargin: root.bottomPadding
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
selectByMouse: !root.ignoreLeftRightKeys
|
||||
clip: true
|
||||
activeFocusOnTab: true
|
||||
KeyNavigation.tab: root.keyNavigationTab
|
||||
KeyNavigation.backtab: root.keyNavigationBacktab
|
||||
onTextChanged: root.textEdited()
|
||||
onEditingFinished: root.editingFinished()
|
||||
onAccepted: root.accepted()
|
||||
onActiveFocusChanged: root.focusStateChanged(activeFocus)
|
||||
Keys.forwardTo: root.keyForwardTargets
|
||||
Keys.onLeftPressed: event => {
|
||||
if (root.ignoreLeftRightKeys) {
|
||||
event.accepted = true
|
||||
} else {
|
||||
// Allow normal TextInput cursor movement
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
Keys.onRightPressed: event => {
|
||||
if (root.ignoreLeftRightKeys) {
|
||||
event.accepted = true
|
||||
} else {
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
|
||||
event.accepted = false
|
||||
for (var i = 0; i < root.keyForwardTargets.length; i++) {
|
||||
if (root.keyForwardTargets[i]) {
|
||||
root.keyForwardTargets[i].Keys.pressed(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: clearButton
|
||||
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: clearArea.containsMouse ? Theme.outlineStrong : "transparent"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: showClearButton && text.length > 0
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: 16
|
||||
color: clearArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
textInput.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeholderLabel
|
||||
|
||||
anchors.fill: textInput
|
||||
text: root.placeholderText
|
||||
font: textInput.font
|
||||
color: placeholderColor
|
||||
verticalAlignment: textInput.verticalAlignment
|
||||
visible: textInput.text.length === 0 && !textInput.activeFocus
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
181
quickshell/Widgets/DankToggle.qml
Normal file
181
quickshell/Widgets/DankToggle.qml
Normal file
@@ -0,0 +1,181 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: toggle
|
||||
|
||||
// API
|
||||
property bool checked: false
|
||||
property bool enabled: true
|
||||
property bool toggling: false
|
||||
property string text: ""
|
||||
property string description: ""
|
||||
property bool hideText: false
|
||||
|
||||
signal clicked
|
||||
signal toggled(bool checked)
|
||||
signal toggleCompleted(bool checked)
|
||||
|
||||
readonly property bool showText: text && !hideText
|
||||
|
||||
readonly property int trackWidth: 52
|
||||
readonly property int trackHeight: 30
|
||||
readonly property int insetCircle: 24
|
||||
|
||||
width: showText ? parent.width : trackWidth
|
||||
height: showText ? 60 : trackHeight
|
||||
|
||||
function handleClick() {
|
||||
if (!enabled) return
|
||||
checked = !checked
|
||||
clicked()
|
||||
toggled(checked)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
radius: showText ? Theme.cornerRadius : 0
|
||||
color: "transparent"
|
||||
visible: showText
|
||||
|
||||
StateLayer {
|
||||
visible: showText
|
||||
disabled: !toggle.enabled
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: parent.radius
|
||||
onClicked: toggle.handleClick()
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: toggleTrack.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
visible: showText
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: toggle.text
|
||||
font.pixelSize: Appearance.fontSize.normal
|
||||
font.weight: Font.Medium
|
||||
opacity: toggle.enabled ? 1 : 0.4
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: toggle.description
|
||||
font.pixelSize: Appearance.fontSize.small
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: Math.min(implicitWidth, toggle.width - 120)
|
||||
visible: toggle.description.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: toggleTrack
|
||||
|
||||
width: showText ? trackWidth : Math.max(parent.width, trackWidth)
|
||||
height: showText ? trackHeight : Math.max(parent.height, trackHeight)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: showText ? Theme.spacingM : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
color: (checked && enabled) ? Theme.primary : Theme.surfaceVariantAlpha
|
||||
opacity: toggling ? 0.6 : (enabled ? 1 : 0.4)
|
||||
|
||||
border.color: (!checked || !enabled) ? Theme.outline : "transparent"
|
||||
|
||||
readonly property int pad: Math.round((height - thumb.width) / 2)
|
||||
readonly property int edgeLeft: pad
|
||||
readonly property int edgeRight: width - thumb.width - pad
|
||||
|
||||
StyledRect {
|
||||
id: thumb
|
||||
|
||||
width: (checked && enabled) ? insetCircle : insetCircle - 4
|
||||
height: (checked && enabled) ? insetCircle : insetCircle - 4
|
||||
radius: Theme.cornerRadius
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
color: (checked && enabled) ? Theme.surface : Theme.outline
|
||||
border.color: (checked && enabled) ? Theme.outline : Theme.outline
|
||||
border.width: (checked && enabled) ? 1 : 2
|
||||
|
||||
x: (checked && enabled) ? toggleTrack.edgeRight : toggleTrack.edgeLeft
|
||||
|
||||
Behavior on x {
|
||||
SequentialAnimation {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||
}
|
||||
ScriptAction {
|
||||
script: {
|
||||
toggle.toggleCompleted(toggle.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: checkIcon
|
||||
anchors.centerIn: parent
|
||||
name: "check"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
filled: true
|
||||
opacity: checked && enabled ? 1 : 0
|
||||
scale: checked && enabled ? 1 : 0.6
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasized
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
disabled: !toggle.enabled
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: parent.radius
|
||||
onClicked: toggle.handleClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
83
quickshell/Widgets/DankTooltip.qml
Normal file
83
quickshell/Widgets/DankTooltip.qml
Normal file
@@ -0,0 +1,83 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
WlrLayershell.namespace: "dms:tooltip"
|
||||
|
||||
property string text: ""
|
||||
property real targetX: 0
|
||||
property real targetY: 0
|
||||
property var targetScreen: null
|
||||
property bool alignLeft: false
|
||||
property bool alignRight: false
|
||||
|
||||
function show(text, x, y, screen, leftAlign, rightAlign) {
|
||||
root.text = text;
|
||||
if (screen) {
|
||||
targetScreen = screen;
|
||||
const screenX = screen.x || 0;
|
||||
targetX = x - screenX;
|
||||
} else {
|
||||
targetScreen = null;
|
||||
targetX = x;
|
||||
}
|
||||
targetY = y;
|
||||
alignLeft = leftAlign ?? false;
|
||||
alignRight = rightAlign ?? false;
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
screen: targetScreen
|
||||
implicitWidth: Math.min(300, Math.max(120, textContent.implicitWidth + Theme.spacingM * 2))
|
||||
implicitHeight: textContent.implicitHeight + Theme.spacingS * 2
|
||||
color: "transparent"
|
||||
visible: false
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
}
|
||||
|
||||
margins {
|
||||
left: {
|
||||
if (alignLeft) return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.width ?? Screen.width) - implicitWidth - Theme.spacingS, targetX)))
|
||||
if (alignRight) return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.width ?? Screen.width) - implicitWidth - Theme.spacingS, targetX - implicitWidth)))
|
||||
return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.width ?? Screen.width) - implicitWidth - Theme.spacingS, targetX - implicitWidth / 2)))
|
||||
}
|
||||
top: {
|
||||
if (alignLeft || alignRight) return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.height ?? Screen.height) - implicitHeight - Theme.spacingS, targetY - implicitHeight / 2)))
|
||||
return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.height ?? Screen.height) - implicitHeight - Theme.spacingS, targetY)))
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
|
||||
Text {
|
||||
id: textContent
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 300 - Theme.spacingM * 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
166
quickshell/Widgets/M3WaveProgress.qml
Normal file
166
quickshell/Widgets/M3WaveProgress.qml
Normal file
@@ -0,0 +1,166 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real value: 0
|
||||
property real lineWidth: 2
|
||||
property real wavelength: 20
|
||||
property real amp: 1.6
|
||||
property real phase: 0.0
|
||||
property bool isPlaying: false
|
||||
property real currentAmp: 1.6
|
||||
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
|
||||
property color fillColor: Theme.primary
|
||||
property color playheadColor: Theme.primary
|
||||
|
||||
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
|
||||
function snap(v) { return Math.round(v * dpr) / dpr }
|
||||
|
||||
readonly property real playX: snap(root.width * root.value)
|
||||
readonly property real midY: snap(height / 2)
|
||||
|
||||
Behavior on currentAmp { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } }
|
||||
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
||||
|
||||
Shape {
|
||||
id: flatTrack
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.trackColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathMove { id: flatStart; x: 0; y: root.midY }
|
||||
PathLine { id: flatEnd; x: root.width; y: root.midY }
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: waveClip
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
readonly property real startX: snap(root.lineWidth/2)
|
||||
readonly property real aaBias: (0.25 / root.dpr)
|
||||
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
x: 0
|
||||
width: waveClip.endX
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
Shape {
|
||||
id: waveShape
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width + 4 * root.wavelength
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
x: waveOffsetX
|
||||
|
||||
ShapePath {
|
||||
id: wavePath
|
||||
strokeColor: root.fillColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathSvg { id: waveSvg; path: "" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: startCap
|
||||
width: snap(root.lineWidth)
|
||||
height: snap(root.lineWidth)
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.startX - width/2
|
||||
y: root.midY - height/2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: endCap
|
||||
width: snap(root.lineWidth)
|
||||
height: snap(root.lineWidth)
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.endX - width/2
|
||||
y: root.midY - height/2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: playhead
|
||||
width: 3.5
|
||||
height: Math.max(root.lineWidth + 12, 16)
|
||||
radius: width / 2
|
||||
color: root.playheadColor
|
||||
x: root.playX - width / 2
|
||||
y: root.midY - height / 2
|
||||
z: 3
|
||||
}
|
||||
|
||||
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
|
||||
function wrapMod(a, m) { let r = a % m; return r < 0 ? r + m : r }
|
||||
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
|
||||
|
||||
FrameAnimation {
|
||||
running: root.visible && (root.isPlaying || root.currentAmp > 0)
|
||||
onTriggered: {
|
||||
if (root.isPlaying) root.phase += 0.03 * frameTime * 60
|
||||
startCap.y = root.midY - startCap.height/2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
endCap.y = root.midY - endCap.height/2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
}
|
||||
}
|
||||
|
||||
function buildStaticWave() {
|
||||
const start = waveClip.startX - 2 * root.wavelength
|
||||
const end = width + 2 * root.wavelength
|
||||
if (end <= start) { waveSvg.path = ""; return }
|
||||
|
||||
const kLocal = k
|
||||
const halfPeriod = root.wavelength / 2
|
||||
function y0(x) { return root.midY + root.currentAmp * Math.sin(kLocal * x) }
|
||||
function dy0(x) { return root.currentAmp * Math.cos(kLocal * x) * kLocal }
|
||||
|
||||
let x0 = start
|
||||
let d = `M ${x0} ${y0(x0)}`
|
||||
while (x0 < end) {
|
||||
const x1 = Math.min(x0 + halfPeriod, end)
|
||||
const dx = x1 - x0
|
||||
const yA = y0(x0), yB = y0(x1)
|
||||
const dyA = dy0(x0), dyB = dy0(x1)
|
||||
const c1x = x0 + dx/3
|
||||
const c1y = yA + (dyA * dx)/3
|
||||
const c2x = x1 - dx/3
|
||||
const c2y = yB - (dyB * dx)/3
|
||||
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`
|
||||
x0 = x1
|
||||
}
|
||||
waveSvg.path = d
|
||||
}
|
||||
|
||||
Component.onCompleted: { currentAmp = isPlaying ? amp : 0; buildStaticWave() }
|
||||
onWidthChanged: { flatStart.x = 0; flatEnd.x = width; buildStaticWave() }
|
||||
onHeightChanged: buildStaticWave()
|
||||
onCurrentAmpChanged: buildStaticWave()
|
||||
onWavelengthChanged: { k = (2 * Math.PI) / Math.max(1e-6, wavelength); buildStaticWave() }
|
||||
}
|
||||
30
quickshell/Widgets/PluginGlobalVar.qml
Normal file
30
quickshell/Widgets/PluginGlobalVar.qml
Normal file
@@ -0,0 +1,30 @@
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string varName
|
||||
property var defaultValue: undefined
|
||||
|
||||
readonly property var value: {
|
||||
const pid = parent?.pluginId ?? ""
|
||||
if (!pid || !PluginService.globalVars[pid]) {
|
||||
return defaultValue
|
||||
}
|
||||
return PluginService.globalVars[pid][varName] ?? defaultValue
|
||||
}
|
||||
|
||||
function set(newValue) {
|
||||
const pid = parent?.pluginId ?? ""
|
||||
if (pid) {
|
||||
PluginService.setGlobalVar(pid, varName, newValue)
|
||||
} else {
|
||||
console.warn("PluginGlobalVar: Cannot set", varName, "- no pluginId from parent")
|
||||
}
|
||||
}
|
||||
|
||||
visible: false
|
||||
width: 0
|
||||
height: 0
|
||||
}
|
||||
22
quickshell/Widgets/StateLayer.qml
Normal file
22
quickshell/Widgets/StateLayer.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
property bool disabled: false
|
||||
property color stateColor: Theme.surfaceText
|
||||
property real cornerRadius: parent && parent.radius !== undefined ? parent.radius : Theme.cornerRadius
|
||||
|
||||
readonly property real stateOpacity: disabled ? 0 : pressed ? 0.12 : containsMouse ? 0.08 : 0
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: disabled ? undefined : Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
color: Qt.rgba(stateColor.r, stateColor.g, stateColor.b, stateOpacity)
|
||||
}
|
||||
}
|
||||
29
quickshell/Widgets/StyledRect.qml
Normal file
29
quickshell/Widgets/StyledRect.qml
Normal file
@@ -0,0 +1,29 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Rectangle {
|
||||
color: "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
readonly property var standardAnimation: {
|
||||
"duration": Appearance.anim.durations.normal,
|
||||
"easing.type": Easing.BezierSpline,
|
||||
"easing.bezierCurve": Appearance.anim.curves.standard
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: standardAnimation.duration
|
||||
easing.type: standardAnimation["easing.type"]
|
||||
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: standardAnimation.duration
|
||||
easing.type: standardAnimation["easing.type"]
|
||||
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
|
||||
}
|
||||
}
|
||||
}
|
||||
50
quickshell/Widgets/StyledText.qml
Normal file
50
quickshell/Widgets/StyledText.qml
Normal file
@@ -0,0 +1,50 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Text {
|
||||
property bool isMonospace: false
|
||||
|
||||
FontLoader {
|
||||
id: interFont
|
||||
source: Qt.resolvedUrl("../assets/fonts/inter/InterVariable.ttf")
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: firaCodeFont
|
||||
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
|
||||
}
|
||||
|
||||
readonly property string resolvedFontFamily: {
|
||||
const requestedFont = isMonospace ? SettingsData.monoFontFamily : SettingsData.fontFamily
|
||||
const defaultFont = isMonospace ? SettingsData.defaultMonoFontFamily : SettingsData.defaultFontFamily
|
||||
|
||||
if (requestedFont === defaultFont) {
|
||||
return isMonospace ? firaCodeFont.name : interFont.name
|
||||
}
|
||||
return requestedFont
|
||||
}
|
||||
|
||||
readonly property var standardAnimation: {
|
||||
"duration": Appearance.anim.durations.normal,
|
||||
"easing.type": Easing.BezierSpline,
|
||||
"easing.bezierCurve": Appearance.anim.curves.standard
|
||||
}
|
||||
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Appearance.fontSize.normal
|
||||
font.family: resolvedFontFamily
|
||||
font.weight: SettingsData.fontWeight
|
||||
wrapMode: Text.WordWrap
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
antialiasing: true
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: standardAnimation.duration
|
||||
easing.type: standardAnimation["easing.type"]
|
||||
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
|
||||
}
|
||||
}
|
||||
}
|
||||
24
quickshell/Widgets/StyledTextMetrics.qml
Normal file
24
quickshell/Widgets/StyledTextMetrics.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
TextMetrics {
|
||||
property bool isMonospace: false
|
||||
|
||||
readonly property string resolvedFontFamily: {
|
||||
const requestedFont = isMonospace ? SettingsData.monoFontFamily : SettingsData.fontFamily
|
||||
const defaultFont = isMonospace ? SettingsData.defaultMonoFontFamily : SettingsData.defaultFontFamily
|
||||
|
||||
if (requestedFont === defaultFont) {
|
||||
const availableFonts = Qt.fontFamilies()
|
||||
if (!availableFonts.includes(requestedFont)) {
|
||||
return isMonospace ? "Monospace" : "DejaVu Sans"
|
||||
}
|
||||
}
|
||||
return requestedFont
|
||||
}
|
||||
|
||||
font.pixelSize: Appearance.fontSize.normal
|
||||
font.family: resolvedFontFamily
|
||||
font.weight: SettingsData.fontWeight
|
||||
}
|
||||
73
quickshell/Widgets/SystemLogo.qml
Normal file
73
quickshell/Widgets/SystemLogo.qml
Normal file
@@ -0,0 +1,73 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string colorOverride: ""
|
||||
property real brightnessOverride: 0.5
|
||||
property real contrastOverride: 1
|
||||
|
||||
readonly property bool hasColorOverride: colorOverride !== ""
|
||||
|
||||
property bool useNerdFont: false
|
||||
property string nerdFontIcon: ""
|
||||
|
||||
IconImage {
|
||||
id: iconImage
|
||||
anchors.fill: parent
|
||||
visible: !root.useNerdFont
|
||||
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
layer.enabled: hasColorOverride
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
colorization: 1
|
||||
colorizationColor: colorOverride
|
||||
brightness: brightnessOverride
|
||||
contrast: contrastOverride
|
||||
}
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
id: nfIcon
|
||||
anchors.centerIn: parent
|
||||
visible: root.useNerdFont
|
||||
name: root.nerdFontIcon
|
||||
size: Math.min(root.width, root.height)
|
||||
color: hasColorOverride ? colorOverride : Theme.surfaceText
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $ID"], (output, exitCode) => {
|
||||
if (exitCode !== 0) return
|
||||
const distroId = output.trim()
|
||||
|
||||
// Nerd fonts are better than images usually
|
||||
const supportedDistroNFs = ["debian", "arch", "archcraft", "fedora", "nixos", "ubuntu", "guix", "gentoo", "endeavouros", "manjaro", "opensuse"]
|
||||
if (supportedDistroNFs.includes(distroId)) {
|
||||
root.useNerdFont = true
|
||||
root.nerdFontIcon = distroId
|
||||
return
|
||||
}
|
||||
|
||||
Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $LOGO"], (logoOutput, logoExitCode) => {
|
||||
if (logoExitCode !== 0) return
|
||||
const logo = logoOutput.trim()
|
||||
if (logo === "cachyos") {
|
||||
iconImage.source = "file:///usr/share/icons/cachyos.svg"
|
||||
return
|
||||
} else if (logo === "guix-icon") {
|
||||
iconImage.source = "file:///run/current-system/profile/share/icons/hicolor/scalable/apps/guix-icon.svg"
|
||||
return
|
||||
}
|
||||
iconImage.source = Quickshell.iconPath(logo, true)
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user