last 7 months of qs changes
This commit is contained in:
parent
2c64563ade
commit
4b90113a54
103 changed files with 3467 additions and 1415 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import QtQuick
|
||||
|
||||
TrackedNotification {
|
||||
renderComponent: StandardNotificationRenderer {}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
95
modules/user/modules/quickshell/shell/notifications/test.qml
Normal file
95
modules/user/modules/quickshell/shell/notifications/test.qml
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue