last 7 months of qs changes

This commit is contained in:
outfoxxed 2025-01-06 00:13:19 -08:00
parent 2c64563ade
commit 4b90113a54
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
103 changed files with 3467 additions and 1415 deletions

View file

@ -0,0 +1,39 @@
import QtQuick
Canvas {
id: root
property real ringFill: 1.0
onRingFillChanged: requestPaint();
renderStrategy: Canvas.Cooperative
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.lineWidth = 2;
ctx.strokeStyle = "#70ffffff";
ctx.beginPath();
const half = Math.round(root.width / 2);
const start = -Math.PI * 0.5;
const endM = ringFill == 0.0 || ringFill == 1.0 ? ringFill : 1.0 - ringFill
ctx.arc(half, half, half - ctx.lineWidth, start, start + 2 * Math.PI * endM, true);
ctx.stroke();
const xMin = Math.min(root.width * 0.3);
const xMax = Math.max(root.width * 0.7);
ctx.strokeStyle = "white";
ctx.beginPath();
ctx.moveTo(xMin, xMin);
ctx.lineTo(xMax, xMax);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(xMax, xMin);
ctx.lineTo(xMin, xMax);
ctx.stroke();
}
}

View file

@ -0,0 +1,35 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
TrackedNotification {
id: root
required property Notification notif;
renderComponent: StandardNotificationRenderer {
notif: root.notif
backer: root
}
function handleDiscard() {
if (!lock.retained) notif.dismiss();
root.discarded();
}
function handleDismiss() {
//handleDiscard();
}
RetainableLock {
id: lock
object: root.notif
locked: true
onRetainedChanged: {
if (retained) root.discard();
}
}
expireTimeout: notif.expireTimeout
}

View file

@ -0,0 +1,243 @@
import QtQuick
import Quickshell
import "../components"
Item {
id: root
enum FlingState {
Inert,
Returning,
Flinging,
Dismissing
}
implicitWidth: display.implicitWidth
// note: can be 0, use ZHVStack
implicitHeight: (display.implicitHeight + padding * 2) * display.meshFactor
z: 1.0 - display.meshFactor
property var view;
property Item contentItem;
property real padding: 5;
property real edgeXOffset;
property bool canOverlap: display.rotation > 2 || Math.abs(display.displayY) > 10 || display.displayX < -60
property bool canDismiss: display.state != FlickableNotification.Dismissing && display.state != FlickableNotification.Flinging;
property alias displayContainer: displayContainer;
signal leftViewBounds();
signal dismissed();
signal discarded();
signal startedFlick();
function playEntry(delay: real) {
if (display.state != FlickableNotification.Flinging) {
display.displayX = -display.width + edgeXOffset
root.playReturn(delay);
}
}
function playDismiss(delay: real) {
if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
display.state = FlickableNotification.Dismissing;
display.animationDelay = delay;
}
}
function playDiscard(delay: real) {
if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
display.velocityX = 500;
display.velocityY = 1500;
display.state = FlickableNotification.Flinging;
display.animationDelay = delay;
}
}
function playReturn(delay: real) {
if (display.state != FlickableNotification.Flinging) {
display.state = FlickableNotification.Returning;
display.animationDelay = delay;
}
}
MouseArea {
id: mouseArea
width: display.width
height: display.height
enabled: display.state == FlickableNotification.Inert || display.state == FlickableNotification.Returning
FlickMonitor {
id: flickMonitor
target: mouseArea
onDragDeltaXChanged: {
const delta = dragDeltaX;
display.displayX = delta < 0 ? delta : Math.pow(delta, 0.8);
display.updateMeshFactor(true);
updateDragY();
}
onDragDeltaYChanged: {
updateDragY();
display.state = FlickableNotification.Inert;
}
function updateDragY() {
//const xMul = 1//dragDeltaX < 0 ? 0 : Math.min(1, Math.pow(dragDeltaX / 200, 0.8));
const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
const xMul = d
const targetY = dragDeltaY;
display.displayY = root.padding + targetY * xMul;
}
onFlickStarted: {
display.initialAnimComplete = true;
root.startedFlick();
}
onFlickCompleted: {
display.releaseY = dragEndY;
if (velocityX > 1000 || (velocityX > -100 && display.displayX > display.width * 0.4)) {
display.velocityX = Math.max(velocityX * 0.8, 1000);
display.velocityY = velocityY * 0.6;
display.state = FlickableNotification.Flinging;
root.discarded();
} else if (velocityX < -1500 || (velocityX < 100 && display.displayX < -(display.width * 0.4))) {
display.velocityX = Math.min(velocityX * 0.8, -700)
display.velocityY = 0
display.state = FlickableNotification.Dismissing;
root.dismissed();
} else {
display.velocityX = 0;
display.velocityY = 0;
display.state = FlickableNotification.Returning;
}
}
}
Item {
id: displayContainer
layer.enabled: view && view.topNotification == root
opacity: layer.enabled ? 0 : 1 // shader ignores it
width: Math.ceil(display.width + display.xPadding * 2)
height: Math.ceil(display.height + display.yPadding * 2)
x: Math.floor(display.targetContainmentX)
y: Math.floor(display.targetContainmentY)
Item {
id: display
//anchors.centerIn: parent
x: xPadding + (targetContainmentX - displayContainer.x)
y: yPadding + (targetContainmentY - displayContainer.y)
//visible: meshFactor > 0.95
children: [root.contentItem]
implicitWidth: root.contentItem?.width ?? 0
implicitHeight: root.contentItem?.height ?? 0
property var state: FlickableNotification.Inert;
property real meshFactor: 1;
property real velocityX;
property real velocityY;
property real releaseY;
property real animationDelay;
property bool initialAnimComplete;
property real displayX;
property real displayY;
property real tiltSize: Math.max(width, height) * 1.2;
property real xPadding: (tiltSize - width) / 2;
property real yPadding: (tiltSize - height) / 2;
property real targetContainmentX: display.displayX - display.xPadding
property real targetContainmentY: root.padding + display.displayY - display.yPadding
function updateMeshFactor(canRemesh: bool) {
let meshFactor = (display.implicitWidth - Math.abs(display.displayX)) / display.implicitWidth;
meshFactor = 0.8 + (meshFactor * 0.2);
meshFactor = Math.max(0, meshFactor);
if (canRemesh) this.meshFactor = meshFactor;
else this.meshFactor = Math.min(this.meshFactor, meshFactor);
}
function unmesh(delta: real) {
if (meshFactor > 0) {
this.meshFactor = Math.max(0, this.meshFactor - delta * 5);
}
}
rotation: display.displayX < 0 ? 0 : display.displayX * (initialAnimComplete ? 0.1 : 0.02)
property real lastX;
FrameAnimation {
function dampingVelocity(currentVelocity, delta) {
const spring = 1.0;
const damping = 0.1;
const springForce = spring * delta;
const dampingForce = -damping * currentVelocity;
return currentVelocity + (springForce + dampingForce);
}
running: display.state != FlickableNotification.Inert
onTriggered: {
let frameTime = this.frameTime;
if (display.animationDelay != 0) {
const usedDelay = Math.min(display.animationDelay, frameTime);
frameTime -= usedDelay;
display.animationDelay -= usedDelay;
if (frameTime == 0) return;
}
if (display.state == FlickableNotification.Flinging) {
display.velocityY += frameTime * 100000 * (1 / display.velocityX * 100);
//display.velocityX -= display.velocityX * 0.98 * frameTime
display.unmesh(frameTime);
} else if (display.state == FlickableNotification.Dismissing) {
const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
display.displayY = root.padding + display.releaseY * d;
display.velocityY = 0;
display.velocityX += frameTime * -20000;
if (display.displayX + display.width > 0) display.updateMeshFactor(false);
else display.unmesh(frameTime);
} else {
const deltaX = 0 - display.displayX;
const deltaY = root.padding - display.displayY;
display.velocityX = dampingVelocity(display.velocityX, deltaX);
display.velocityY = dampingVelocity(display.velocityY, deltaY);
if (Math.abs(display.velocityX) < 0.01 && Math.abs(deltaX) < 1
&& Math.abs(display.velocityY) < 0.01 && Math.abs(deltaY) < 1) {
display.state = FlickableNotification.Inert;
display.displayX = 0;
display.displayY = root.padding;
display.velocityX = 0;
display.velocityY = 0;
display.initialAnimComplete = true;
}
display.updateMeshFactor(true);
}
display.displayX += display.velocityX * frameTime;
display.displayY += display.velocityY * frameTime;
// todo: actually base this on the viewport
if (display.displayX > 10000 || display.displayY > 10000 || (display.displayX + display.width < root.edgeXOffset && display.meshFactor == 0) || display.displayY < -10000) root.leftViewBounds();
}
}
}
}
}
}

View file

@ -0,0 +1,126 @@
import QtQuick
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import "../components"
import "../shaders" as Shaders
Item {
id: root
property list<Item> notifications: [];
property list<Item> heightStack: [];
property alias stack: stack;
property alias topNotification: stack.topNotification;
function addNotificationInert(notification: TrackedNotification): Item {
const harness = stack._harnessComponent.createObject(stack, {
backer: notification,
view: root,
});
harness.contentItem = notification.renderComponent.createObject(harness);
notifications = [...notifications, harness];
heightStack = [harness, ...heightStack];
return harness;
}
function addNotification(notification: TrackedNotification) {
const harness = root.addNotificationInert(notification);
harness.playEntry(0);
}
function dismissAll() {
let delay = 0;
for (const notification of root.notifications) {
if (!notification.canDismiss) continue;
notification.playDismiss(delay);
notification.dismissed();
delay += 0.025;
}
}
function discardAll() {
let delay = 0;
for (const notification of root.notifications) {
if (!notification.canDismiss) continue;
notification.playDismiss(delay);
notification.discarded();
delay += 0.025;
}
}
function addSet(notifications: list<TrackedNotification>) {
let delay = 0;
for (const notification of notifications) {
if (notification.visualizer) {
notification.visualizer.playReturn(delay);
} else {
const harness = root.addNotificationInert(notification);
harness.playEntry(delay);
}
delay += 0.025;
}
}
Item {
anchors.fill: parent
layer.enabled: stack.topNotification != null
layer.effect: Shaders.MaskedOverlay {
overlayItem: stack.topNotification?.displayContainer ?? null
overlayPos: Qt.point(stack.x + stack.topNotification.x + overlayItem.x, stack.y + stack.topNotification.y + overlayItem.y)
}
ZHVStack {
id: stack
property Item topNotification: {
if (root.heightStack.length < 2) return null;
const top = root.heightStack[0] ?? null;
return top && top.canOverlap ? top : null;
};
property Component _harnessComponent: FlickableNotification {
id: notification
required property TrackedNotification backer;
edgeXOffset: -stack.x
onDismissed: backer.handleDismiss();
onDiscarded: backer.handleDiscard();
onLeftViewBounds: {
root.notifications = root.notifications.filter(n => n != this);
root.heightStack = root.heightStack.filter(n => n != this);
this.destroy();
}
onStartedFlick: {
root.heightStack = [this, ...root.heightStack.filter(n => n != this)];
}
Component.onCompleted: backer.visualizer = this;
Connections {
target: backer
function onDismiss() {
notification.playDismiss(0);
notification.dismissed();
}
function onDiscard() {
notification.playDismiss(0);
notification.discarded();
}
}
}
}
}
}

View file

@ -0,0 +1,74 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
Singleton {
id: root
property list<TrackedNotification> notifications;
property Component notifComponent: DaemonNotification {}
property bool showTrayNotifs: false;
property bool dnd: false;
property bool hasNotifs: root.notifications.length != 0
property var lastHoveredNotif;
property var overlay;
signal notif(notif: TrackedNotification);
signal showAll(notifications: list<TrackedNotification>);
signal dismissAll(notifications: list<TrackedNotification>);
signal discardAll(notifications: list<TrackedNotification>);
NotificationServer {
imageSupported: true
actionsSupported: true
actionIconsSupported: true
onNotification: notification => {
notification.tracked = true;
const notif = root.notifComponent.createObject(null, { notif: notification });
root.notifications = [...root.notifications, notif];
root.notif(notif);
}
}
Instantiator {
model: root.notifications
Connections {
required property TrackedNotification modelData;
target: modelData;
function onDiscarded() {
root.notifications = root.notifications.filter(n => n != target);
modelData.untrack();
}
function onDiscard() {
if (!modelData.visualizer) modelData.discarded();
}
}
}
onShowTrayNotifsChanged: {
if (showTrayNotifs) {
for (const notif of root.notifications) {
notif.inTray = true;
}
root.showAll(root.notifications);
} else {
root.dismissAll(root.notifications);
}
}
function sendDiscardAll() {
root.discardAll(root.notifications);
}
}

View file

@ -0,0 +1,39 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
PanelWindow {
WlrLayershell.namespace: "shell:notifications"
exclusionMode: ExclusionMode.Ignore
color: "transparent"
anchors {
left: true
top: true
bottom: true
right: true
}
property Component notifComponent: DaemonNotification {}
NotificationDisplay {
id: display
anchors.fill: parent
stack.y: 5 + 55//(NotificationManager.showTrayNotifs ? 55 : 0)
stack.x: 72
}
visible: display.stack.children.length != 0
mask: Region { item: display.stack }
Component.onCompleted: {
NotificationManager.overlay = this;
NotificationManager.notif.connect(display.addNotification);
NotificationManager.showAll.connect(display.addSet);
NotificationManager.dismissAll.connect(display.dismissAll);
NotificationManager.discardAll.connect(display.discardAll);
}
}

View file

@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Controls
import "root:bar"
BarWidgetInner {
id: root
required property var bar;
property bool controlsOpen: false;
onControlsOpenChanged: NotificationManager.showTrayNotifs = controlsOpen;
Connections {
target: NotificationManager
function onHasNotifsChanged() {
if (!NotificationManager.hasNotifs) {
root.controlsOpen = false;
}
}
}
implicitHeight: width
BarButton {
id: button
anchors.fill: parent
baseMargin: 8
fillWindowWidth: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
showPressed: root.controlsOpen || (pressedButtons & ~Qt.RightButton)
Image {
anchors.fill: parent
source: NotificationManager.hasNotifs
? "root:icons/bell-fill.svg"
: "root:icons/bell.svg"
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
}
onPressed: event => {
if (event.button == Qt.RightButton && NotificationManager.hasNotifs) {
root.controlsOpen = !root.controlsOpen;
}
}
}
property var tooltip: TooltipItem {
tooltip: bar.tooltip
owner: root
show: button.containsMouse
Label {
anchors.verticalCenter: parent.verticalCenter
text: {
const count = NotificationManager.notifications.length;
return count == 0 ? "No notifications"
: count == 1 ? "1 notification"
: `${count} notifications`;
}
}
}
property var rightclickMenu: TooltipItem {
tooltip: bar.tooltip
owner: root
isMenu: true
grabWindows: [NotificationManager.overlay]
show: root.controlsOpen
onClose: root.controlsOpen = false
Item {
implicitWidth: 440
implicitHeight: root.implicitHeight - 10
MouseArea {
id: closeArea
anchors {
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
implicitWidth: 30
implicitHeight: 30
hoverEnabled: true
onPressed: {
NotificationManager.sendDiscardAll()
}
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: width * 0.5
antialiasing: true
color: "#60ffffff"
opacity: closeArea.containsMouse ? 1 : 0
Behavior on opacity { SmoothedAnimation { velocity: 8 } }
}
CloseButton {
anchors.fill: parent
ringFill: root.backer.timePercentage
}
}
}
}
}

View file

@ -0,0 +1,203 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Notifications
import ".."
Rectangle {
id: root
required property Notification notif;
required property var backer;
color: notif.urgency == NotificationUrgency.Critical ? "#30ff2030" : "#30c0ffff"
radius: 5
implicitWidth: 450
implicitHeight: c.implicitHeight
HoverHandler {
onHoveredChanged: {
backer.pauseCounter += hovered ? 1 : -1;
}
}
Rectangle {
id: border
anchors.fill: parent
color: "transparent"
border.width: 2
border.color: ShellGlobals.colors.widgetOutline
radius: root.radius
}
ColumnLayout {
id: c
anchors.fill: parent
spacing: 0
ColumnLayout {
Layout.margins: 10
RowLayout {
Image {
visible: source != ""
source: notif.appIcon ? Quickshell.iconPath(notif.appIcon) : ""
fillMode: Image.PreserveAspectFit
antialiasing: true
sourceSize.width: 30
sourceSize.height: 30
Layout.preferredWidth: 30
Layout.preferredHeight: 30
}
Label {
visible: text != ""
text: notif.summary
font.pointSize: 20
elide: Text.ElideRight
Layout.maximumWidth: root.implicitWidth - 100 // QTBUG-127649
}
Item { Layout.fillWidth: true }
MouseArea {
id: closeArea
Layout.preferredWidth: 30
Layout.preferredHeight: 30
hoverEnabled: true
onPressed: root.backer.discard();
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: width * 0.5
antialiasing: true
color: "#60ffffff"
opacity: closeArea.containsMouse ? 1 : 0
Behavior on opacity { SmoothedAnimation { velocity: 8 } }
}
CloseButton {
anchors.fill: parent
ringFill: root.backer.timePercentage
}
}
}
Item {
Layout.topMargin: 3
visible: bodyLabel.text != "" || notifImage.visible
implicitWidth: bodyLabel.width
implicitHeight: Math.max(notifImage.size, bodyLabel.implicitHeight)
Image {
id: notifImage
readonly property int size: visible ? 14 * 8 : 0
y: bodyLabel.y + bodyLabel.topPadding
visible: source != ""
source: notif.image
fillMode: Image.PreserveAspectFit
cache: false
antialiasing: true
width: size
height: size
sourceSize.width: size
sourceSize.height: size
}
Label {
id: bodyLabel
width: root.implicitWidth - 20
text: notif.body
wrapMode: Text.Wrap
onLineLaidOut: line => {
if (!notifImage.visible) return;
const isize = notifImage.size + 6;
if (line.y + line.height <= notifImage.y + isize) {
line.x += isize;
line.width -= isize;
}
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.margins: root.border.width
spacing: 0
visible: notif.actions.length != 0
Rectangle {
height: border.border.width
Layout.fillWidth: true
color: border.border.color
antialiasing: true
}
RowLayout {
spacing: 0
Repeater {
model: notif.actions
Item {
required property NotificationAction modelData;
required property int index;
Layout.fillWidth: true
implicitHeight: 35
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: -implicitWidth * 0.5
}
visible: index != 0
implicitWidth: root.border.width
color: ShellGlobals.colors.widgetOutline
antialiasing: true
}
MouseArea {
id: actionArea
anchors.fill: parent
onClicked: {
modelData.invoke();
}
Rectangle {
anchors.fill: parent
color: actionArea.pressed && actionArea.containsMouse ? "#20000000" : "transparent"
}
RowLayout {
anchors.centerIn: parent
Image {
visible: notif.hasActionIcons
source: Quickshell.iconPath(modelData.identifier)
fillMode: Image.PreserveAspectFit
antialiasing: true
sourceSize.height: 25
sourceSize.width: 25
}
Label { text: modelData.text }
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,5 @@
import QtQuick
TrackedNotification {
renderComponent: StandardNotificationRenderer {}
}

View file

@ -0,0 +1,65 @@
import QtQuick
import Quickshell
Scope {
id: root
required property Component renderComponent;
property bool inTray: false;
property bool destroyOnInvisible: false;
property int visualizerCount: 0;
property FlickableNotification visualizer;
signal dismiss();
signal discard();
signal discarded();
function handleDismiss() {}
function handleDiscard() {}
onVisualizerChanged: {
if (!visualizer) {
expireAnim.stop();
timePercentage = 1;
}
if (!visualizer && destroyOnInvisible) this.destroy();
}
function untrack() {
destroyOnInvisible = true;
if (!visualizer) this.destroy();
}
property int expireTimeout: -1
property real timePercentage: 1
property int pauseCounter: 0
readonly property bool shouldPause: root.pauseCounter != 0 || (NotificationManager.lastHoveredNotif?.pauseCounter ?? 0) != 0
onPauseCounterChanged: {
if (pauseCounter > 0) {
NotificationManager.lastHoveredNotif = this;
}
}
NumberAnimation on timePercentage {
id: expireAnim
running: expireTimeout != 0
paused: running && root.shouldPause && to == 0
duration: expireTimeout == -1 ? 10000 : expireTimeout
to: 0
onFinished: {
if (!inTray) root.dismiss();
}
}
onInTrayChanged: {
if (inTray) {
expireAnim.stop();
expireAnim.duration = 300 * (1 - timePercentage);
expireAnim.to = 1;
expireAnim.start();
}
}
}

View file

@ -0,0 +1,95 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "../components"
ShellRoot {
Component {
id: demoNotif
FlickableNotification {
contentItem: Rectangle {
color: "white"
border.color: "blue"
border.width: 2
radius: 10
width: 400
height: 150
}
onLeftViewBounds: this.destroy()
}
}
property Component testComponent: TrackedNotification {
id: notification
renderComponent: Rectangle {
color: "white"
border.color: "blue"
border.width: 2
radius: 10
width: 400
height: 150
ColumnLayout {
Button {
text: "dismiss"
onClicked: notification.dismiss();
}
Button {
text: "discard"
onClicked: notification.discard();
}
}
}
function handleDismiss() {
console.log(`dismiss (sub)`)
}
function handleDiscard() {
console.log(`discard (sub)`)
}
Component.onDestruction: console.log(`destroy (sub)`)
};
property Component realComponent: DaemonNotification {
id: dn
}
Daemon {
onNotification: notification => {
notification.tracked = true;
const o = realComponent.createObject(null, { notif: notification });
display.addNotification(o);
}
}
FloatingWindow {
color: "transparent"
ColumnLayout {
x: 5
Button {
visible: false
text: "add notif"
onClicked: {
//const notif = demoNotif.createObject(stack);
//stack.children = [...stack.children, notif];
const notif = testComponent.createObject(null);
display.addNotification(notif);
}
}
//ZHVStack { id: stack }
NotificationDisplay { id: display }
}
}
}