quickshell: should be everything I want now
This commit is contained in:
28
quickshell/Noctalia/NBox.qml
Normal file
28
quickshell/Noctalia/NBox.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
|
||||
// Rounded group container using the variant surface color.
|
||||
// To be used in side panels and settings panes to group fields or buttons.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool compact: false
|
||||
|
||||
implicitWidth: childrenRect.width
|
||||
implicitHeight: childrenRect.height
|
||||
color: compact ? Color.transparent : Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
layer.enabled: !compact
|
||||
|
||||
layer.effect: DropShadow {
|
||||
horizontalOffset: 6
|
||||
verticalOffset: 6
|
||||
radius: 8
|
||||
samples: 12
|
||||
color: Qt.rgba(0, 0, 0, 0.3)
|
||||
}
|
||||
|
||||
}
|
||||
53
quickshell/Noctalia/NBusyIndicator.qml
Normal file
53
quickshell/Noctalia/NBusyIndicator.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
import QtQuick
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool running: true
|
||||
property color color: Color.mPrimary
|
||||
property int size: Style.baseWidgetSize
|
||||
property int strokeWidth: Style.borderL
|
||||
property int duration: Style.animationSlow * 2
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
|
||||
Canvas {
|
||||
id: canvas
|
||||
|
||||
property real rotationAngle: 0
|
||||
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
var centerX = width / 2;
|
||||
var centerY = height / 2;
|
||||
var radius = Math.min(width, height) / 2 - strokeWidth / 2;
|
||||
ctx.strokeStyle = root.color;
|
||||
ctx.lineWidth = Math.max(1, root.strokeWidth);
|
||||
ctx.lineCap = "round";
|
||||
// Draw arc with gap (270 degrees with 90 degree gap)
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, -Math.PI / 2 + rotationAngle, -Math.PI / 2 + rotationAngle + Math.PI * 1.5);
|
||||
ctx.stroke();
|
||||
}
|
||||
onRotationAngleChanged: {
|
||||
requestPaint();
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: canvas
|
||||
property: "rotationAngle"
|
||||
running: root.running
|
||||
from: 0
|
||||
to: 2 * Math.PI
|
||||
duration: root.duration
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
122
quickshell/Noctalia/NCircleStat.qml
Normal file
122
quickshell/Noctalia/NCircleStat.qml
Normal file
@@ -0,0 +1,122 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Noctalia
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
|
||||
// Compact circular statistic display using Layout management
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real value: 0 // 0..100 (or any range visually mapped)
|
||||
property string icon: ""
|
||||
property string suffix: "%"
|
||||
// When nested inside a parent group (NBox), you can make it flat
|
||||
property bool flat: false
|
||||
// Scales the internal content (labels, gauge, icon) without changing the
|
||||
// outer width/height footprint of the component
|
||||
property real contentScale: 1
|
||||
|
||||
width: 68
|
||||
height: 92
|
||||
color: flat ? Color.transparent : Color.mSurface
|
||||
radius: Style.radiusS
|
||||
border.color: flat ? Color.transparent : Color.mSurfaceVariant
|
||||
border.width: flat ? 0 : Math.max(1, Style.borderS)
|
||||
// Repaint gauge when the bound value changes
|
||||
onValueChanged: gauge.requestPaint()
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * contentScale
|
||||
spacing: 0
|
||||
|
||||
// Main gauge container
|
||||
Item {
|
||||
id: gaugeContainer
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: 68 * contentScale
|
||||
Layout.preferredHeight: 68 * contentScale
|
||||
|
||||
Canvas {
|
||||
// 390° (equivalent to 30°)
|
||||
|
||||
id: gauge
|
||||
|
||||
anchors.fill: parent
|
||||
renderStrategy: Canvas.Cooperative
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
const w = width, h = height;
|
||||
const cx = w / 2, cy = h / 2;
|
||||
const r = Math.min(w, h) / 2 - 5 * contentScale;
|
||||
// Rotated 90° to the right: gap at the bottom
|
||||
// Start at 150° and end at 390° (30°) → bottom opening
|
||||
const start = Math.PI * 5 / 6;
|
||||
// 150°
|
||||
const endBg = Math.PI * 13 / 6;
|
||||
ctx.reset();
|
||||
ctx.lineWidth = 6 * contentScale;
|
||||
// Track uses surfaceVariant for stronger contrast
|
||||
ctx.strokeStyle = Color.mSurface;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, start, endBg);
|
||||
ctx.stroke();
|
||||
// Value arc
|
||||
const ratio = Math.max(0, Math.min(1, root.value / 100));
|
||||
const end = start + (endBg - start) * ratio;
|
||||
ctx.strokeStyle = Color.mPrimary;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, start, end);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// Percent centered in the circle
|
||||
NText {
|
||||
id: valueLabel
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: -4 * contentScale
|
||||
text: `${root.value}${root.suffix}`
|
||||
pointSize: Style.fontSizeM * contentScale
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Tiny circular badge for the icon, positioned inside below the percentage
|
||||
Rectangle {
|
||||
id: iconBadge
|
||||
|
||||
width: iconText.implicitWidth + Style.marginXXS
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: Color.mPrimary
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: valueLabel.bottom
|
||||
anchors.topMargin: 8 * contentScale
|
||||
|
||||
NIcon {
|
||||
id: iconText
|
||||
|
||||
anchors.centerIn: parent
|
||||
icon: root.icon
|
||||
color: Color.mOnPrimary
|
||||
pointSize: Style.fontSizeS
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
124
quickshell/Noctalia/NContextMenu.qml
Normal file
124
quickshell/Noctalia/NContextMenu.qml
Normal file
@@ -0,0 +1,124 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property alias model: listView.model
|
||||
property real itemHeight: 36
|
||||
property real itemPadding: Style.marginM
|
||||
|
||||
signal triggered(string action)
|
||||
|
||||
// Helper function to open at mouse position
|
||||
function openAt(x, y) {
|
||||
root.x = x;
|
||||
root.y = y;
|
||||
root.open();
|
||||
}
|
||||
|
||||
// Helper function to open at item
|
||||
function openAtItem(item, mouseX, mouseY) {
|
||||
var pos = item.mapToItem(root.parent, mouseX || 0, mouseY || 0);
|
||||
openAt(pos.x, pos.y);
|
||||
}
|
||||
|
||||
width: 180
|
||||
padding: Style.marginS
|
||||
onOpened: PanelService.willOpenPopup(root)
|
||||
onClosed: PanelService.willClosePopup(root)
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS)
|
||||
radius: Style.radiusM
|
||||
}
|
||||
|
||||
contentItem: NListView {
|
||||
id: listView
|
||||
|
||||
implicitHeight: contentHeight
|
||||
spacing: Style.marginXXS
|
||||
interactive: contentHeight > root.height
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: menuItem
|
||||
|
||||
// Store reference to the popup
|
||||
property var popup: root
|
||||
|
||||
width: listView.width
|
||||
height: modelData.visible !== false ? root.itemHeight : 0
|
||||
visible: modelData.visible !== false
|
||||
opacity: modelData.enabled !== false ? 1 : 0.5
|
||||
enabled: modelData.enabled !== false
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
popup.triggered(modelData.action || modelData.key || index.toString());
|
||||
popup.close();
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: menuItem.hovered && menuItem.enabled ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusS
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Style.marginS
|
||||
|
||||
// Optional icon
|
||||
NIcon {
|
||||
visible: modelData.icon !== undefined
|
||||
icon: modelData.icon || ""
|
||||
pointSize: Style.fontSizeM
|
||||
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
|
||||
Layout.leftMargin: root.itemPadding
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.label || modelData.text || ""
|
||||
pointSize: Style.fontSizeM
|
||||
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
35
quickshell/Noctalia/NDivider.qml
Normal file
35
quickshell/Noctalia/NDivider.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Constants
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: Math.max(1, Style.borderS)
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Color.transparent
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 0.1
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 0.9
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: Color.transparent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
28
quickshell/Noctalia/NIcon.qml
Normal file
28
quickshell/Noctalia/NIcon.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property string icon: Icons.defaultIcon
|
||||
property real pointSize: Style.fontSizeL
|
||||
|
||||
visible: (icon !== undefined) && (icon !== "")
|
||||
text: {
|
||||
if ((icon === undefined) || (icon === ""))
|
||||
return "";
|
||||
|
||||
if (Icons.get(icon) === undefined) {
|
||||
Logger.warn("Icon", `"${icon}"`, "doesn't exist in the icons font");
|
||||
Logger.callStack();
|
||||
return Icons.get(Icons.defaultIcon);
|
||||
}
|
||||
return Icons.get(icon);
|
||||
}
|
||||
font.family: Icons.fontFamily
|
||||
font.pointSize: root.pointSize
|
||||
color: Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
92
quickshell/Noctalia/NIconButton.qml
Normal file
92
quickshell/Noctalia/NIconButton.qml
Normal file
@@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real baseSize: Style.baseWidgetSize
|
||||
property string icon
|
||||
property bool enabled: true
|
||||
property bool allowClickWhenDisabled: false
|
||||
property bool hovering: false
|
||||
property bool compact: false
|
||||
property color colorBg: Color.mSurfaceVariant
|
||||
property color colorFg: Color.mPrimary
|
||||
property color colorBgHover: Color.mTertiary
|
||||
property color colorFgHover: Color.mOnTertiary
|
||||
property color colorBorder: Color.transparent
|
||||
property color colorBorderHover: Color.transparent
|
||||
|
||||
signal entered()
|
||||
signal exited()
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
signal middleClicked()
|
||||
|
||||
implicitWidth: Math.round(baseSize)
|
||||
implicitHeight: Math.round(baseSize)
|
||||
opacity: root.enabled ? Style.opacityFull : Style.opacityMedium
|
||||
color: root.enabled && root.hovering ? colorBgHover : colorBg
|
||||
radius: width * 0.5
|
||||
border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder
|
||||
border.width: Math.max(1, Style.borderS)
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
pointSize: Math.max(1, root.compact ? root.width * 0.65 : root.width * 0.48)
|
||||
color: root.enabled && root.hovering ? colorFgHover : colorFg
|
||||
// Center horizontally
|
||||
x: (root.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (root.height - height) / 2 + (height - contentHeight) / 2
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// Always enabled to allow hover/tooltip even when the button is disabled
|
||||
enabled: true
|
||||
anchors.fill: parent
|
||||
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
hovering = root.enabled ? true : false;
|
||||
root.entered();
|
||||
}
|
||||
onExited: {
|
||||
hovering = false;
|
||||
root.exited();
|
||||
}
|
||||
onClicked: function(mouse) {
|
||||
if (!root.enabled && !allowClickWhenDisabled)
|
||||
return ;
|
||||
|
||||
if (mouse.button === Qt.LeftButton)
|
||||
root.clicked();
|
||||
else if (mouse.button === Qt.RightButton)
|
||||
root.rightClicked();
|
||||
else if (mouse.button === Qt.MiddleButton)
|
||||
root.middleClicked();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
85
quickshell/Noctalia/NImageCircled.qml
Normal file
85
quickshell/Noctalia/NImageCircled.qml
Normal file
@@ -0,0 +1,85 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string imagePath: ""
|
||||
property color borderColor: Color.transparent
|
||||
property real borderWidth: 0
|
||||
property string fallbackIcon: ""
|
||||
property real fallbackIconSize: Style.fontSizeXXL
|
||||
|
||||
color: Color.transparent
|
||||
radius: parent.width * 0.5
|
||||
anchors.margins: Style.marginXXS
|
||||
|
||||
Rectangle {
|
||||
color: Color.transparent
|
||||
anchors.fill: parent
|
||||
|
||||
Image {
|
||||
id: img
|
||||
|
||||
anchors.fill: parent
|
||||
source: imagePath
|
||||
visible: false // Hide since we're using it as shader source
|
||||
mipmap: true
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
antialiasing: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
}
|
||||
|
||||
ShaderEffect {
|
||||
property var source
|
||||
property real imageOpacity: root.opacity
|
||||
|
||||
anchors.fill: parent
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/circled_image.frag.qsb")
|
||||
supportsAtlasTextures: false
|
||||
blending: true
|
||||
|
||||
source: ShaderEffectSource {
|
||||
sourceItem: img
|
||||
hideSource: true
|
||||
live: true
|
||||
recursive: false
|
||||
format: ShaderEffectSource.RGBA
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fallback icon
|
||||
Loader {
|
||||
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: fallbackIcon
|
||||
pointSize: fallbackIconSize
|
||||
z: 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Border
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Color.transparent
|
||||
border.color: parent.borderColor
|
||||
border.width: parent.borderWidth
|
||||
antialiasing: true
|
||||
z: 10
|
||||
}
|
||||
|
||||
}
|
||||
103
quickshell/Noctalia/NImageRounded.qml
Normal file
103
quickshell/Noctalia/NImageRounded.qml
Normal file
@@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string imagePath: ""
|
||||
property color borderColor: Color.transparent
|
||||
property real borderWidth: 0
|
||||
property real imageRadius: width * 0.5
|
||||
property string fallbackIcon: ""
|
||||
property real fallbackIconSize: Style.fontSizeXXL
|
||||
property real scaledRadius: imageRadius
|
||||
|
||||
signal statusChanged(int status)
|
||||
|
||||
color: Color.transparent
|
||||
radius: scaledRadius
|
||||
anchors.margins: Style.marginXXS
|
||||
|
||||
Rectangle {
|
||||
color: Color.transparent
|
||||
anchors.fill: parent
|
||||
|
||||
Image {
|
||||
id: img
|
||||
|
||||
anchors.fill: parent
|
||||
source: imagePath
|
||||
visible: false // Hide since we're using it as shader source
|
||||
mipmap: true
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
antialiasing: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
onStatusChanged: root.statusChanged(status)
|
||||
}
|
||||
|
||||
ShaderEffect {
|
||||
property var source
|
||||
// Use custom property names to avoid conflicts with final properties
|
||||
property real itemWidth: root.width
|
||||
property real itemHeight: root.height
|
||||
property real cornerRadius: root.radius
|
||||
property real imageOpacity: root.opacity
|
||||
|
||||
anchors.fill: parent
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
|
||||
// Qt6 specific properties - ensure proper blending
|
||||
supportsAtlasTextures: false
|
||||
blending: true
|
||||
|
||||
// Make sure the background is transparent
|
||||
Rectangle {
|
||||
id: background
|
||||
|
||||
anchors.fill: parent
|
||||
color: Color.transparent
|
||||
z: -1
|
||||
}
|
||||
|
||||
source: ShaderEffectSource {
|
||||
sourceItem: img
|
||||
hideSource: true
|
||||
live: true
|
||||
recursive: false
|
||||
format: ShaderEffectSource.RGBA
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fallback icon
|
||||
Loader {
|
||||
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: fallbackIcon
|
||||
pointSize: fallbackIconSize
|
||||
z: 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Border
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Color.transparent
|
||||
border.color: parent.borderColor
|
||||
border.width: parent.borderWidth
|
||||
antialiasing: true
|
||||
z: 10
|
||||
}
|
||||
|
||||
}
|
||||
217
quickshell/Noctalia/NListView.qml
Normal file
217
quickshell/Noctalia/NListView.qml
Normal file
@@ -0,0 +1,217 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Templates as T
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color handleColor: Qt.alpha(Color.mTertiary, 0.8)
|
||||
property color handleHoverColor: handleColor
|
||||
property color handlePressedColor: handleColor
|
||||
property color trackColor: Color.transparent
|
||||
property real handleWidth: 6
|
||||
property real handleRadius: Style.radiusM
|
||||
property int verticalPolicy: ScrollBar.AsNeeded
|
||||
property int horizontalPolicy: ScrollBar.AsNeeded
|
||||
// Forward ListView properties
|
||||
property alias model: listView.model
|
||||
property alias delegate: listView.delegate
|
||||
property alias spacing: listView.spacing
|
||||
property alias orientation: listView.orientation
|
||||
property alias currentIndex: listView.currentIndex
|
||||
property alias count: listView.count
|
||||
property alias contentHeight: listView.contentHeight
|
||||
property alias contentWidth: listView.contentWidth
|
||||
property alias contentY: listView.contentY
|
||||
property alias contentX: listView.contentX
|
||||
property alias currentItem: listView.currentItem
|
||||
property alias highlightItem: listView.highlightItem
|
||||
property alias headerItem: listView.headerItem
|
||||
property alias footerItem: listView.footerItem
|
||||
property alias section: listView.section
|
||||
property alias highlightFollowsCurrentItem: listView.highlightFollowsCurrentItem
|
||||
property alias highlightMoveDuration: listView.highlightMoveDuration
|
||||
property alias highlightMoveVelocity: listView.highlightMoveVelocity
|
||||
property alias preferredHighlightBegin: listView.preferredHighlightBegin
|
||||
property alias preferredHighlightEnd: listView.preferredHighlightEnd
|
||||
property alias highlightRangeMode: listView.highlightRangeMode
|
||||
property alias snapMode: listView.snapMode
|
||||
property alias keyNavigationWraps: listView.keyNavigationWraps
|
||||
property alias cacheBuffer: listView.cacheBuffer
|
||||
property alias displayMarginBeginning: listView.displayMarginBeginning
|
||||
property alias displayMarginEnd: listView.displayMarginEnd
|
||||
property alias layoutDirection: listView.layoutDirection
|
||||
property alias effectiveLayoutDirection: listView.effectiveLayoutDirection
|
||||
property alias verticalLayoutDirection: listView.verticalLayoutDirection
|
||||
property alias boundsBehavior: listView.boundsBehavior
|
||||
property alias flickableDirection: listView.flickableDirection
|
||||
property alias interactive: listView.interactive
|
||||
property alias moving: listView.moving
|
||||
property alias flicking: listView.flicking
|
||||
property alias dragging: listView.dragging
|
||||
property alias horizontalVelocity: listView.horizontalVelocity
|
||||
property alias verticalVelocity: listView.verticalVelocity
|
||||
|
||||
// Forward ListView methods
|
||||
function positionViewAtIndex(index, mode) {
|
||||
listView.positionViewAtIndex(index, mode);
|
||||
}
|
||||
|
||||
function positionViewAtBeginning() {
|
||||
listView.positionViewAtBeginning();
|
||||
}
|
||||
|
||||
function positionViewAtEnd() {
|
||||
listView.positionViewAtEnd();
|
||||
}
|
||||
|
||||
function forceLayout() {
|
||||
listView.forceLayout();
|
||||
}
|
||||
|
||||
function cancelFlick() {
|
||||
listView.cancelFlick();
|
||||
}
|
||||
|
||||
function flick(xVelocity, yVelocity) {
|
||||
listView.flick(xVelocity, yVelocity);
|
||||
}
|
||||
|
||||
function incrementCurrentIndex() {
|
||||
listView.incrementCurrentIndex();
|
||||
}
|
||||
|
||||
function decrementCurrentIndex() {
|
||||
listView.decrementCurrentIndex();
|
||||
}
|
||||
|
||||
function indexAt(x, y) {
|
||||
return listView.indexAt(x, y);
|
||||
}
|
||||
|
||||
function itemAt(x, y) {
|
||||
return listView.itemAt(x, y);
|
||||
}
|
||||
|
||||
function itemAtIndex(index) {
|
||||
return listView.itemAtIndex(index);
|
||||
}
|
||||
|
||||
// Set reasonable implicit sizes for Layout usage
|
||||
implicitWidth: 200
|
||||
implicitHeight: 200
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
anchors.fill: parent
|
||||
// Enable clipping to keep content within bounds
|
||||
clip: true
|
||||
// Enable flickable for smooth scrolling
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: listView
|
||||
x: listView.mirrored ? 0 : listView.width - width
|
||||
y: 0
|
||||
height: listView.height
|
||||
active: listView.ScrollBar.horizontal.active
|
||||
policy: root.verticalPolicy
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: 100
|
||||
radius: root.handleRadius
|
||||
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: 100
|
||||
color: root.trackColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0
|
||||
radius: root.handleRadius / 2
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
id: horizontalScrollBar
|
||||
|
||||
parent: listView
|
||||
x: 0
|
||||
y: listView.height - height
|
||||
width: listView.width
|
||||
active: listView.ScrollBar.vertical.active
|
||||
policy: root.horizontalPolicy
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 100
|
||||
implicitHeight: root.handleWidth
|
||||
radius: root.handleRadius
|
||||
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 100
|
||||
implicitHeight: root.handleWidth
|
||||
color: root.trackColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0
|
||||
radius: root.handleRadius / 2
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
459
quickshell/Noctalia/NPanel.qml
Normal file
459
quickshell/Noctalia/NPanel.qml
Normal file
@@ -0,0 +1,459 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Noctalia
|
||||
import qs.Services
|
||||
import qs.Constants
|
||||
import qs.Utils
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
property Component panelContent: null
|
||||
property real preferredWidth: 700
|
||||
property real preferredHeight: 900
|
||||
property real preferredWidthRatio
|
||||
property real preferredHeightRatio
|
||||
property color panelBackgroundColor: Color.mSurface
|
||||
property bool draggable: false
|
||||
property var buttonItem: null
|
||||
property string buttonName: ""
|
||||
|
||||
property bool panelAnchorHorizontalCenter: false
|
||||
property bool panelAnchorVerticalCenter: false
|
||||
property bool panelAnchorTop: false
|
||||
property bool panelAnchorBottom: false
|
||||
property bool panelAnchorLeft: false
|
||||
property bool panelAnchorRight: false
|
||||
|
||||
property bool isMasked: false
|
||||
|
||||
// Properties to support positioning relative to the opener (button)
|
||||
property bool useButtonPosition: false
|
||||
property point buttonPosition: Qt.point(0, 0)
|
||||
property int buttonWidth: 0
|
||||
property int buttonHeight: 0
|
||||
|
||||
property bool panelKeyboardFocus: false
|
||||
property bool backgroundClickEnabled: true
|
||||
|
||||
// Animation properties
|
||||
readonly property real originalScale: 0.7
|
||||
readonly property real originalOpacity: 0.0
|
||||
property real scaleValue: originalScale
|
||||
property real opacityValue: originalOpacity
|
||||
property real dimmingOpacity: 0
|
||||
|
||||
signal opened
|
||||
signal closed
|
||||
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
Component.onCompleted: {
|
||||
PanelService.registerPanel(root)
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Functions to control background click behavior
|
||||
function disableBackgroundClick() {
|
||||
backgroundClickEnabled = false
|
||||
}
|
||||
|
||||
function enableBackgroundClick() {
|
||||
// Add a small delay to prevent immediate close after drag release
|
||||
enableBackgroundClickTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: enableBackgroundClickTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: backgroundClickEnabled = true
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
function toggle(buttonItem, buttonName) {
|
||||
if (!active) {
|
||||
open(buttonItem, buttonName)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
function open(buttonItem, buttonName) {
|
||||
root.buttonItem = buttonItem
|
||||
root.buttonName = buttonName || ""
|
||||
|
||||
setPosition()
|
||||
|
||||
PanelService.willOpenPanel(root)
|
||||
|
||||
backgroundClickEnabled = true
|
||||
active = true
|
||||
root.opened()
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
function close() {
|
||||
dimmingOpacity = 0
|
||||
scaleValue = originalScale
|
||||
opacityValue = originalOpacity
|
||||
root.closed()
|
||||
active = false
|
||||
useButtonPosition = false
|
||||
backgroundClickEnabled = true
|
||||
PanelService.closedPanel(root)
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
function setPosition() {
|
||||
// If we have a button name, we are landing here from an IPC call.
|
||||
// IPC calls have no idead on which screen they panel will spawn.
|
||||
// Resolve the button name to a proper button item now that we have a screen.
|
||||
if (buttonName !== "" && root.screen !== null) {
|
||||
buttonItem = BarService.lookupWidget(buttonName, root.screen.name)
|
||||
}
|
||||
|
||||
// Get the button position if provided
|
||||
if (buttonItem !== undefined && buttonItem !== null) {
|
||||
useButtonPosition = true
|
||||
var itemPos = buttonItem.mapToItem(null, 0, 0)
|
||||
buttonPosition = Qt.point(itemPos.x, itemPos.y)
|
||||
buttonWidth = buttonItem.width
|
||||
buttonHeight = buttonItem.height
|
||||
} else {
|
||||
useButtonPosition = false
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
sourceComponent: Component {
|
||||
PanelWindow {
|
||||
id: panelWindow
|
||||
|
||||
readonly property bool isVertical: false
|
||||
readonly property bool barIsVisible: (screen !== null)
|
||||
readonly property real verticalBarWidth: Math.round(Style.barHeight)
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("NPanel", "Opened", root.objectName)
|
||||
dimmingOpacity = Style.opacityHeavy
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: panelWindow
|
||||
function onScreenChanged() {
|
||||
root.screen = screen
|
||||
|
||||
// If called from IPC always reposition if screen is updated
|
||||
if (buttonName) {
|
||||
setPosition()
|
||||
}
|
||||
// Logger.log("NPanel", "OnScreenChanged", root.screen.name)
|
||||
}
|
||||
}
|
||||
|
||||
visible: true
|
||||
color: Qt.alpha(Color.mShadow, dimmingOpacity)
|
||||
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "noctalia-panel"
|
||||
WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
|
||||
mask: root.isMasked ? maskRegion : null
|
||||
|
||||
Region {
|
||||
id: maskRegion
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
}
|
||||
}
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
|
||||
// Close any panel with Esc without requiring focus
|
||||
Shortcut {
|
||||
sequences: ["Escape"]
|
||||
enabled: root.active
|
||||
onActivated: root.close()
|
||||
context: Qt.WindowShortcut
|
||||
}
|
||||
|
||||
// Clicking outside of the rectangle to close
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.backgroundClickEnabled
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
// The actual panel's content
|
||||
Rectangle {
|
||||
id: panelBackground
|
||||
color: panelBackgroundColor
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS )
|
||||
// Dragging support
|
||||
property bool draggable: root.draggable
|
||||
property bool isDragged: false
|
||||
property real manualX: 0
|
||||
property real manualY: 0
|
||||
width: {
|
||||
var w
|
||||
if (preferredWidthRatio !== undefined) {
|
||||
w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth) )
|
||||
} else {
|
||||
w = preferredWidth
|
||||
}
|
||||
// Clamp width so it is never bigger than the screen
|
||||
return Math.min(w, screen?.width - Style.marginL * 2)
|
||||
}
|
||||
height: {
|
||||
var h
|
||||
if (preferredHeightRatio !== undefined) {
|
||||
h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight) )
|
||||
} else {
|
||||
h = preferredHeight
|
||||
}
|
||||
|
||||
// Clamp width so it is never bigger than the screen
|
||||
return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2)
|
||||
}
|
||||
|
||||
scale: root.scaleValue
|
||||
opacity: root.isMasked ? 0 : root.opacityValue
|
||||
x: isDragged ? manualX : calculatedX
|
||||
y: isDragged ? manualY : calculatedY
|
||||
|
||||
// ---------------------------------------------
|
||||
// Does not account for corners are they are negligible and helps keep the code clean.
|
||||
// ---------------------------------------------
|
||||
property real marginTop: {
|
||||
if (!barIsVisible) {
|
||||
return 0
|
||||
}
|
||||
return (Style.barHeight + Style.marginS)
|
||||
}
|
||||
|
||||
property real marginBottom: {
|
||||
if (!barIsVisible) {
|
||||
return 0
|
||||
}
|
||||
return Style.marginS
|
||||
}
|
||||
|
||||
property real marginLeft: {
|
||||
if (!barIsVisible) {
|
||||
return 0
|
||||
}
|
||||
return Style.marginS
|
||||
}
|
||||
|
||||
property real marginRight: {
|
||||
if (!barIsVisible) {
|
||||
return 0
|
||||
}
|
||||
return Style.marginS
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
property int calculatedX: {
|
||||
// Priority to fixed anchoring
|
||||
if (panelAnchorHorizontalCenter) {
|
||||
// Center horizontally but respect bar margins
|
||||
var centerX = Math.round((panelWindow.width - panelBackground.width) / 2)
|
||||
var minX = marginLeft
|
||||
var maxX = panelWindow.width - panelBackground.width - marginRight
|
||||
return Math.round(Math.max(minX, Math.min(centerX, maxX)))
|
||||
} else if (panelAnchorLeft) {
|
||||
return marginLeft
|
||||
} else if (panelAnchorRight) {
|
||||
return Math.round(panelWindow.width - panelBackground.width - marginRight)
|
||||
}
|
||||
|
||||
// No fixed anchoring
|
||||
if (isVertical) {
|
||||
// Vertical bar
|
||||
if (barPosition === "right") {
|
||||
// To the left of the right bar
|
||||
return Math.round(panelWindow.width - panelBackground.width - marginRight)
|
||||
} else {
|
||||
// To the right of the left bar
|
||||
return marginLeft
|
||||
}
|
||||
} else {
|
||||
// Horizontal bar
|
||||
if (root.useButtonPosition) {
|
||||
// Position panel relative to button
|
||||
var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2)
|
||||
// Keep panel within screen bounds
|
||||
var maxX = panelWindow.width - panelBackground.width - marginRight
|
||||
var minX = marginLeft
|
||||
return Math.round(Math.max(minX, Math.min(targetX, maxX)))
|
||||
} else {
|
||||
// Fallback to center horizontally
|
||||
return Math.round((panelWindow.width - panelBackground.width) / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
property int calculatedY: {
|
||||
// Priority to fixed anchoring
|
||||
if (panelAnchorVerticalCenter) {
|
||||
// Center vertically but respect bar margins
|
||||
var centerY = Math.round((panelWindow.height - panelBackground.height) / 2)
|
||||
var minY = marginTop
|
||||
var maxY = panelWindow.height - panelBackground.height - marginBottom
|
||||
return Math.round(Math.max(minY, Math.min(centerY, maxY)))
|
||||
} else if (panelAnchorTop) {
|
||||
return marginTop
|
||||
} else if (panelAnchorBottom) {
|
||||
return Math.round(panelWindow.height - panelBackground.height - marginBottom)
|
||||
}
|
||||
|
||||
// No fixed anchoring
|
||||
if (isVertical) {
|
||||
// Vertical bar
|
||||
if (useButtonPosition) {
|
||||
// Position panel relative to button
|
||||
var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2)
|
||||
// Keep panel within screen bounds
|
||||
var maxY = panelWindow.height - panelBackground.height - marginBottom
|
||||
var minY = marginTop
|
||||
return Math.round(Math.max(minY, Math.min(targetY, maxY)))
|
||||
} else {
|
||||
// Fallback to center vertically
|
||||
return Math.round((panelWindow.height - panelBackground.height) / 2)
|
||||
}
|
||||
} else {
|
||||
return marginTop
|
||||
}
|
||||
}
|
||||
|
||||
// Animate in when component is completed
|
||||
Component.onCompleted: {
|
||||
root.scaleValue = 1.0
|
||||
root.opacityValue = 1.0
|
||||
}
|
||||
|
||||
// Reset drag position when panel closes
|
||||
Connections {
|
||||
target: root
|
||||
function onClosed() {
|
||||
panelBackground.isDragged = false
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent closing when clicking in the panel bg
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
// Animation behaviors
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: panelContentLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.panelContent
|
||||
}
|
||||
|
||||
// Handle drag move on the whole panel area
|
||||
DragHandler {
|
||||
id: dragHandler
|
||||
target: null
|
||||
enabled: panelBackground.draggable
|
||||
property real dragStartX: 0
|
||||
property real dragStartY: 0
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
// Capture current position into manual coordinates BEFORE toggling isDragged
|
||||
panelBackground.manualX = panelBackground.x
|
||||
panelBackground.manualY = panelBackground.y
|
||||
dragStartX = panelBackground.x
|
||||
dragStartY = panelBackground.y
|
||||
panelBackground.isDragged = true
|
||||
if (root.enableBackgroundClick)
|
||||
root.disableBackgroundClick()
|
||||
} else {
|
||||
// Keep isDragged true so we continue using the manual x/y after release
|
||||
if (root.enableBackgroundClick)
|
||||
root.enableBackgroundClick()
|
||||
}
|
||||
}
|
||||
onTranslationChanged: {
|
||||
// Proposed new coordinates from fixed drag origin
|
||||
var nx = dragStartX + translation.x
|
||||
var ny = dragStartY + translation.y
|
||||
|
||||
// Calculate gaps so we never overlap the bar on any side
|
||||
var baseGap = Style.marginS
|
||||
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL : 0
|
||||
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL : 0
|
||||
|
||||
var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight + floatExtraH) : 0)
|
||||
var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight + floatExtraH) : 0)
|
||||
var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight + floatExtraV) : 0)
|
||||
var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight + floatExtraV) : 0)
|
||||
|
||||
// Clamp within screen bounds accounting for insets
|
||||
var maxX = panelWindow.width - panelBackground.width - insetRight
|
||||
var minX = insetLeft
|
||||
var maxY = panelWindow.height - panelBackground.height - insetBottom
|
||||
var minY = insetTop
|
||||
|
||||
panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX)))
|
||||
panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY)))
|
||||
}
|
||||
}
|
||||
|
||||
// Drag indicator border
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 0
|
||||
color: Color.transparent
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(2, Style.borderL )
|
||||
radius: parent.radius
|
||||
visible: panelBackground.isDragged && dragHandler.active
|
||||
opacity: 0.8
|
||||
z: 3000
|
||||
|
||||
// Subtle glow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 0
|
||||
color: Color.transparent
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderS )
|
||||
radius: parent.radius
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
quickshell/Noctalia/NSlider.qml
Normal file
152
quickshell/Noctalia/NSlider.qml
Normal file
@@ -0,0 +1,152 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
|
||||
Slider {
|
||||
id: root
|
||||
|
||||
property var cutoutColor: Color.mSurface
|
||||
property bool snapAlways: true
|
||||
property real heightRatio: 0.7
|
||||
readonly property real knobDiameter: Math.round((Style.baseWidgetSize * heightRatio) / 2) * 2
|
||||
readonly property real trackHeight: Math.round((knobDiameter * 0.4) / 2) * 2
|
||||
readonly property real cutoutExtra: Math.round((Style.baseWidgetSize * 0.1) / 2) * 2
|
||||
|
||||
padding: cutoutExtra / 2
|
||||
snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease
|
||||
implicitHeight: Math.max(trackHeight, knobDiameter)
|
||||
|
||||
background: Rectangle {
|
||||
x: root.leftPadding
|
||||
y: root.topPadding + root.availableHeight / 2 - height / 2
|
||||
implicitWidth: Style.sliderWidth
|
||||
implicitHeight: trackHeight
|
||||
width: root.availableWidth
|
||||
height: implicitHeight
|
||||
radius: height / 2
|
||||
color: Qt.alpha(Color.mSurface, 0.5)
|
||||
border.color: Qt.alpha(Color.mOutline, 0.5)
|
||||
border.width: Math.max(1, Style.borderS)
|
||||
|
||||
// A container composite shape that puts a semicircle on the end
|
||||
Item {
|
||||
id: activeTrackContainer
|
||||
|
||||
width: root.visualPosition * parent.width
|
||||
height: parent.height
|
||||
|
||||
// The rounded end cap made from a rounded rectangle
|
||||
Rectangle {
|
||||
width: parent.height
|
||||
height: parent.height
|
||||
radius: width / 2
|
||||
color: Qt.darker(Color.mPrimary, 1.2) //starting color of gradient
|
||||
}
|
||||
|
||||
// The main rectangle
|
||||
Rectangle {
|
||||
x: parent.height / 2
|
||||
width: parent.width - x // Fills the rest of the container
|
||||
height: parent.height
|
||||
radius: 0
|
||||
|
||||
// Animated gradient fill
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Qt.darker(Color.mPrimary, 1.2)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 300
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 0.5
|
||||
color: Color.mPrimary
|
||||
|
||||
SequentialAnimation on position {
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
from: 0.3
|
||||
to: 0.7
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
from: 0.7
|
||||
to: 0.3
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: Qt.lighter(Color.mPrimary, 1.2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Circular cutout
|
||||
Rectangle {
|
||||
id: knobCutout
|
||||
|
||||
implicitWidth: knobDiameter + cutoutExtra
|
||||
implicitHeight: knobDiameter + cutoutExtra
|
||||
radius: width / 2
|
||||
color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface
|
||||
x: root.leftPadding + root.visualPosition * (root.availableWidth - root.knobDiameter) - cutoutExtra
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handle: Item {
|
||||
implicitWidth: knobDiameter
|
||||
implicitHeight: knobDiameter
|
||||
x: root.leftPadding + root.visualPosition * (root.availableWidth - width)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: knob
|
||||
|
||||
implicitWidth: knobDiameter
|
||||
implicitHeight: knobDiameter
|
||||
radius: width / 2
|
||||
color: root.pressed ? Color.mTertiary : Color.mSurface
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderL)
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
20
quickshell/Noctalia/NText.qml
Normal file
20
quickshell/Noctalia/NText.qml
Normal file
@@ -0,0 +1,20 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property string family: Fonts.primary
|
||||
property real pointSize: Style.fontSizeM
|
||||
property real fontScale: 1
|
||||
|
||||
font.family: root.family
|
||||
font.weight: Style.fontWeightMedium
|
||||
font.pointSize: root.pointSize * fontScale
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Reference in New Issue
Block a user