ported some quickshell stuff from rivendell-hyprdots
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
documentation.man.generateCaches = true; # used for the man script
|
documentation.man.generateCaches = true; # used for the man script
|
||||||
|
|
||||||
|
qt.enable = true;
|
||||||
|
|
||||||
# grub theme
|
# grub theme
|
||||||
boot.loader.grub = {
|
boot.loader.grub = {
|
||||||
theme = inputs.nixos-grub-themes.packages.${pkgs.system}.nixos; # if you want to use nixos grub theme
|
theme = inputs.nixos-grub-themes.packages.${pkgs.system}.nixos; # if you want to use nixos grub theme
|
||||||
@@ -211,6 +213,10 @@
|
|||||||
manix
|
manix
|
||||||
deadnix
|
deadnix
|
||||||
alejandra
|
alejandra
|
||||||
|
# libs or apps for quickshell
|
||||||
|
libsForQt5.qt5.qtgraphicaleffects
|
||||||
|
mprisence
|
||||||
|
kdePackages.qt5compat
|
||||||
];
|
];
|
||||||
|
|
||||||
# fonts
|
# fonts
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#../../modules/home-manager/neovim2.nix # ./neovim2.nix uses vim plug instead of the nix repository as with ./neovim.nix see the begining of neovim2 for an explanation
|
#../../modules/home-manager/neovim2.nix # ./neovim2.nix uses vim plug instead of the nix repository as with ./neovim.nix see the begining of neovim2 for an explanation
|
||||||
../../modules/home-manager/rclone.nix
|
../../modules/home-manager/rclone.nix
|
||||||
../../modules/home-manager/waybar.nix
|
../../modules/home-manager/waybar.nix
|
||||||
|
../../modules/home-manager/quickshell.nix # used for widgets. Maybe it will replace waybar
|
||||||
#../../modules/home-manager/oh-my-posh.nix # zsh customizer
|
#../../modules/home-manager/oh-my-posh.nix # zsh customizer
|
||||||
../../modules/home-manager/oh-my-zsh.nix # another zsh customizer
|
../../modules/home-manager/oh-my-zsh.nix # another zsh customizer
|
||||||
../../modules/home-manager/hypridle.nix
|
../../modules/home-manager/hypridle.nix
|
||||||
|
|||||||
@@ -228,6 +228,9 @@ in {
|
|||||||
pseudotile = true;
|
pseudotile = true;
|
||||||
preserve_split = true;
|
preserve_split = true;
|
||||||
};
|
};
|
||||||
|
scrolling = {
|
||||||
|
column_width = 0.45;
|
||||||
|
};
|
||||||
|
|
||||||
# █▀▄▀█ █ █▀ █▀▀
|
# █▀▄▀█ █ █▀ █▀▀
|
||||||
# █░▀░█ █ ▄█ █▄▄
|
# █░▀░█ █ ▄█ █▄▄
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# pulled from https://codeberg.org/zacoons/rivendell-hyprdots
|
||||||
|
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
qt.enable =true;
|
||||||
|
programs.quickshell = {
|
||||||
|
systemd.enable = true;
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
home.file.".config/quickshell" = {
|
||||||
|
source = ../../other/quickshell;
|
||||||
|
recursive = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import "./"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
property var modelData
|
||||||
|
screen: modelData
|
||||||
|
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
aboveWindows: false
|
||||||
|
color: "transparent"
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
bottom: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
id: bg
|
||||||
|
anchors.fill: parent
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
source: `./assets/${Globals.isDarkTheme ? "darkbg" : "bg"}.png`
|
||||||
|
smooth: false
|
||||||
|
}
|
||||||
|
MultiEffect {
|
||||||
|
source: bg
|
||||||
|
anchors.fill: bg
|
||||||
|
blurEnabled: false
|
||||||
|
blurMax: 24
|
||||||
|
blur: 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: timetxt
|
||||||
|
x: 900
|
||||||
|
y: 150
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
font.pointSize: 100
|
||||||
|
font.bold: true
|
||||||
|
color: Globals.isDarkTheme ? "#9292B6" : "#3B2640"
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: dateProc
|
||||||
|
command: ["date", "+%H %M"]
|
||||||
|
running: true
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => timetxt.text = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
interval: 1000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: dateProc.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var getThemeProc: Process {
|
||||||
|
running: true
|
||||||
|
command: ["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"]
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => root.isDarkTheme = data == "'prefer-dark'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
property var isDarkTheme: false
|
||||||
|
readonly property var kittenThemeProc: Process {
|
||||||
|
command: ["kitten", "theme", "--reload-in=all", `"Gruvbox ${root.isDarkTheme ? "Dark" : "Light"} Soft"`]
|
||||||
|
}
|
||||||
|
readonly property var gtk4ThemeProc: Process {
|
||||||
|
command: ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", `"prefer-${root.isDarkTheme ? "dark" : "light"}"`]
|
||||||
|
}
|
||||||
|
readonly property var gtk3ThemeProc: Process {
|
||||||
|
command: ["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", `"Adwaita${root.isDarkTheme ? "-dark" : ""}"`]
|
||||||
|
}
|
||||||
|
onIsDarkThemeChanged: () => {
|
||||||
|
kittenThemeProc.command = ["kitten", "theme", "--reload-in=all", `Gruvbox ${root.isDarkTheme ? "Dark" : "Light"} Soft`];
|
||||||
|
kittenThemeProc.running = true;
|
||||||
|
gtk4ThemeProc.command = ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", `"prefer-${root.isDarkTheme ? "dark" : "light"}"`];
|
||||||
|
gtk4ThemeProc.running = true;
|
||||||
|
gtk3ThemeProc.command = ["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", `"Adwaita${root.isDarkTheme ? "-dark" : ""}"`];
|
||||||
|
gtk3ThemeProc.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 157 B |
|
After Width: | Height: | Size: 176 B |
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 842 B |
|
After Width: | Height: | Size: 160 B |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 383 B |
|
After Width: | Height: | Size: 143 B |
|
After Width: | Height: | Size: 505 B |
|
After Width: | Height: | Size: 285 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 148 B |
|
After Width: | Height: | Size: 167 B |
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 114 B |
|
After Width: | Height: | Size: 141 B |
|
After Width: | Height: | Size: 206 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 172 B |
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 188 B |
|
After Width: | Height: | Size: 164 B |
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 281 B |
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1,166 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "mpris" as Mpris
|
||||||
|
import "systray" as SysTray
|
||||||
|
import ".."
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var modelData
|
||||||
|
readonly property var tooltip: tooltip
|
||||||
|
readonly property real tooltipYOffset: root.height + 2
|
||||||
|
Tooltip {
|
||||||
|
id: tooltip
|
||||||
|
bar: root
|
||||||
|
}
|
||||||
|
function boundedY(targetY: real, height: real): real {
|
||||||
|
return Math.max(bar.anchors.topMargin + height, Math.min(bar.height + bar.anchors.topMargin - height, targetY));
|
||||||
|
}
|
||||||
|
function boundedX(targetX: real, width: real): real {
|
||||||
|
return Math.max(bar.anchors.leftMargin + width, Math.min(bar.width + bar.anchors.leftMargin - width, targetX));
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "shell:bar"
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
exclusiveZone: 48
|
||||||
|
implicitHeight: 72
|
||||||
|
color: "transparent"
|
||||||
|
mask: barRegion
|
||||||
|
|
||||||
|
Region {
|
||||||
|
id: barRegion
|
||||||
|
width: bar.width
|
||||||
|
height: bar.height
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: bar
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: 48
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
left: parent.left
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: left.width + left.anchors.leftMargin + left.anchors.rightMargin
|
||||||
|
implicitHeight: left.height
|
||||||
|
color: "#3B253F"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
z: -1
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#BE850E"
|
||||||
|
anchors.margins: -4
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
width: 16 * 6
|
||||||
|
height: 4 * 6
|
||||||
|
anchors.top: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
source: "../assets/bar-decor.png"
|
||||||
|
smooth: false
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: left
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 7
|
||||||
|
rightMargin: 7
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Launcher {}
|
||||||
|
|
||||||
|
Workspaces {
|
||||||
|
bar: root
|
||||||
|
}
|
||||||
|
|
||||||
|
SysTray.SysTray {
|
||||||
|
bar: root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: right.width + right.anchors.leftMargin + right.anchors.rightMargin
|
||||||
|
implicitHeight: right.height
|
||||||
|
color: "#3B253F"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
z: -1
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#BE850E"
|
||||||
|
anchors.margins: -4
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
width: 16 * 6
|
||||||
|
height: 4 * 6
|
||||||
|
anchors.top: parent.bottom
|
||||||
|
anchors.right: parent.right
|
||||||
|
source: "../assets/bar-decor.png"
|
||||||
|
smooth: false
|
||||||
|
mirror: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: right
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
right: parent.right
|
||||||
|
leftMargin: 7
|
||||||
|
rightMargin: 7
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Time {}
|
||||||
|
|
||||||
|
Volume {
|
||||||
|
bar: root
|
||||||
|
}
|
||||||
|
|
||||||
|
Mpris.Players {
|
||||||
|
window: root
|
||||||
|
bar: root
|
||||||
|
}
|
||||||
|
|
||||||
|
Theme {}
|
||||||
|
|
||||||
|
Power {
|
||||||
|
id: power
|
||||||
|
bar: root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
|
||||||
|
FullwidthMouseArea {
|
||||||
|
id: root
|
||||||
|
property bool showPressed: mouseArea.pressed
|
||||||
|
property real baseMargin: 0
|
||||||
|
property bool directScale: false
|
||||||
|
|
||||||
|
readonly property Item contentItem: mContentItem
|
||||||
|
default property alias contentItemData: mContentItem.data
|
||||||
|
|
||||||
|
property real targetBrightness: root.showPressed ? -25 : root.mouseArea.containsMouse && root.enabled ? 75 : 0
|
||||||
|
Behavior on targetBrightness {
|
||||||
|
SmoothedAnimation {
|
||||||
|
velocity: 600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property real targetMargins: root.showPressed ? 3 : 0
|
||||||
|
Behavior on targetMargins {
|
||||||
|
SmoothedAnimation {
|
||||||
|
velocity: 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: mContentItem
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
anchors.margins: root.baseMargin + (root.directScale ? 0 : root.targetMargins)
|
||||||
|
scale: root.directScale ? (width - root.targetMargins * 2) / width : 1.0
|
||||||
|
|
||||||
|
opacity: root.enabled ? 1.0 : 0.5
|
||||||
|
Behavior on opacity {
|
||||||
|
SmoothedAnimation {
|
||||||
|
velocity: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.enabled: root.targetBrightness != 0
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
brightness: root.targetBrightness / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
BarButton {
|
||||||
|
id: root
|
||||||
|
property alias image: img.source
|
||||||
|
property alias mirror: img.mirror
|
||||||
|
implicitWidth: 24
|
||||||
|
implicitHeight: 24
|
||||||
|
Image {
|
||||||
|
id: img
|
||||||
|
anchors.fill: parent
|
||||||
|
smooth: false
|
||||||
|
scale: root.containsMouse ? 0.9 : 1
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool fillWindowWidth: false
|
||||||
|
property real extraVerticalMargin: 0
|
||||||
|
|
||||||
|
property alias mouseArea: mouseArea
|
||||||
|
property alias hoverEnabled: mouseArea.hoverEnabled
|
||||||
|
property alias acceptedButtons: mouseArea.acceptedButtons
|
||||||
|
property alias pressedButtons: mouseArea.pressedButtons
|
||||||
|
property alias containsMouse: mouseArea.containsMouse
|
||||||
|
property alias isPressed: mouseArea.pressed
|
||||||
|
|
||||||
|
signal clicked(event: MouseEvent)
|
||||||
|
signal entered
|
||||||
|
signal exited
|
||||||
|
signal pressed(event: MouseEvent)
|
||||||
|
signal released(event: MouseEvent)
|
||||||
|
signal wheel(event: WheelEvent)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
// not much point in finding exact values
|
||||||
|
leftMargin: root.fillWindowWidth ? -50 : 0
|
||||||
|
rightMargin: root.fillWindowWidth ? -50 : 0
|
||||||
|
topMargin: -root.extraVerticalMargin
|
||||||
|
bottomMargin: -root.extraVerticalMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
this.clicked.connect(root.clicked);
|
||||||
|
//this.entered.connect(root.entered);
|
||||||
|
//this.exited.connect(root.exited);
|
||||||
|
//this.pressed.connect(root.pressed);
|
||||||
|
this.released.connect(root.released);
|
||||||
|
//this.wheel.connect(root.wheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for some reason MouseArea.pressed is both a prop and signal so connect doesn't work
|
||||||
|
onPressed: event => root.pressed(event)
|
||||||
|
|
||||||
|
// connecting to onwheel seems to implicitly accept it. undo that.
|
||||||
|
onWheel: event => {
|
||||||
|
event.accepted = false;
|
||||||
|
root.wheel(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import "../launcher" as Launcher
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitWidth: 26
|
||||||
|
implicitHeight: 26
|
||||||
|
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: img
|
||||||
|
anchors.fill: parent
|
||||||
|
source: "../assets/chi-ro.png"
|
||||||
|
smooth: false
|
||||||
|
scale: Launcher.Controller.isOpen ? 0.8 : 1
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MultiEffect {
|
||||||
|
source: img
|
||||||
|
anchors.fill: img
|
||||||
|
blur: 1.0
|
||||||
|
brightness: 0.3
|
||||||
|
blurEnabled: true
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: () => {
|
||||||
|
Launcher.Controller.isOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias mouseArea: mouseArea
|
||||||
|
property alias hoverEnabled: mouseArea.hoverEnabled
|
||||||
|
property alias acceptedButtons: mouseArea.acceptedButtons
|
||||||
|
property alias pressedButtons: mouseArea.pressedButtons
|
||||||
|
property alias containsMouse: mouseArea.containsMouse
|
||||||
|
property alias isPressed: mouseArea.pressed
|
||||||
|
|
||||||
|
signal clicked(event: MouseEvent)
|
||||||
|
signal entered
|
||||||
|
signal exited
|
||||||
|
signal pressed(event: MouseEvent)
|
||||||
|
signal released(event: MouseEvent)
|
||||||
|
signal wheel(event: WheelEvent)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
this.clicked.connect(root.clicked);
|
||||||
|
//this.entered.connect(root.entered);
|
||||||
|
//this.exited.connect(root.exited);
|
||||||
|
//this.pressed.connect(root.pressed);
|
||||||
|
this.released.connect(root.released);
|
||||||
|
//this.wheel.connect(root.wheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For some reason MouseArea.pressed is both a prop and signal so connect doesn't work
|
||||||
|
onPressed: event => root.pressed(event)
|
||||||
|
|
||||||
|
// Connecting to onwheel seems to implicitly accept it. undo that.
|
||||||
|
onWheel: event => {
|
||||||
|
event.accepted = false;
|
||||||
|
root.wheel(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var bar
|
||||||
|
|
||||||
|
readonly property var chargeState: UPower.displayDevice.state
|
||||||
|
readonly property bool isCharging: chargeState == UPowerDeviceState.Charging
|
||||||
|
readonly property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge
|
||||||
|
readonly property real percentage: UPower.displayDevice.percentage
|
||||||
|
readonly property bool isLow: percentage <= 0.20
|
||||||
|
readonly property bool isLaptop: UPowerDevice.isLaptopBattery
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: mask
|
||||||
|
source: "../assets/battery-mask.png"
|
||||||
|
smooth: false
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: progressBar
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "black"
|
||||||
|
Rectangle {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: root.isLaptop ? root.percentage * parent.height : parent.height
|
||||||
|
color: root.isLaptop ? (root.isLow ? "red" : "#EEA939") : "#EEA939"
|
||||||
|
}
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
OpacityMask {
|
||||||
|
id: pBarMask
|
||||||
|
anchors.fill: progressBar
|
||||||
|
source: progressBar
|
||||||
|
maskSource: mask
|
||||||
|
}
|
||||||
|
MultiEffect {
|
||||||
|
source: pBarMask
|
||||||
|
anchors.fill: pBarMask
|
||||||
|
brightness: root.isCharging ? 0.3 : 0
|
||||||
|
blur: 1.0
|
||||||
|
blurEnabled: root.isCharging
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// kind of like a lighter StackView which handles replacement better.
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property Component enterTransition: XAnimator {
|
||||||
|
from: root.width
|
||||||
|
duration: 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
property Component exitTransition: XAnimator {
|
||||||
|
to: target.x - target.width
|
||||||
|
duration: 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool animate: this.visible;
|
||||||
|
|
||||||
|
onAnimateChanged: {
|
||||||
|
if (!this.animate) this.finishAnimations();
|
||||||
|
}
|
||||||
|
|
||||||
|
property Component itemComponent: SlideViewItem {}
|
||||||
|
property SlideViewItem activeItem: null;
|
||||||
|
property Item pendingItem: null;
|
||||||
|
property bool pendingNoAnim: false;
|
||||||
|
property list<SlideViewItem> removingItems;
|
||||||
|
|
||||||
|
readonly property bool animating: activeItem?.activeAnimation != null
|
||||||
|
|
||||||
|
function replace(component: Component, defaults: var, noanim: bool) {
|
||||||
|
this.pendingNoAnim = noanim;
|
||||||
|
|
||||||
|
if (component) {
|
||||||
|
const props = defaults ?? {};
|
||||||
|
props.parent = null;
|
||||||
|
props.width = Qt.binding(() => this.width);
|
||||||
|
props.height = Qt.binding(() => this.height);
|
||||||
|
|
||||||
|
const item = component.createObject(this, props);
|
||||||
|
if (pendingItem) pendingItem.destroy();
|
||||||
|
pendingItem = item;
|
||||||
|
const ready = item?.svReady ?? true;
|
||||||
|
if (ready) finishPending();
|
||||||
|
} else {
|
||||||
|
finishPending(); // remove
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: pendingItem
|
||||||
|
|
||||||
|
function onSvReadyChanged() {
|
||||||
|
if (pendingItem.svReady) {
|
||||||
|
root.finishPending();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishPending() {
|
||||||
|
const noanim = this.pendingNoAnim || !this.animate;
|
||||||
|
if (this.activeItem) {
|
||||||
|
if (noanim) {
|
||||||
|
this.activeItem.destroyAll();
|
||||||
|
this.activeItem = null;
|
||||||
|
} else {
|
||||||
|
removingItems.push(this.activeItem);
|
||||||
|
this.activeItem.animationCompleted.connect(item => root.removeItem(item));
|
||||||
|
this.activeItem.stopIfRunning();
|
||||||
|
this.activeItem.createAnimation(exitTransition);
|
||||||
|
this.activeItem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.animate) finishAnimations();
|
||||||
|
|
||||||
|
if (this.pendingItem) {
|
||||||
|
pendingItem.parent = this;
|
||||||
|
this.activeItem = itemComponent.createObject(this, { item: this.pendingItem });
|
||||||
|
this.pendingItem = null;
|
||||||
|
if (!noanim) {
|
||||||
|
this.activeItem.createAnimation(enterTransition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(item: SlideViewItem) {
|
||||||
|
item.destroyAll();
|
||||||
|
|
||||||
|
for (const i = 0; i !== this.removingItems.length; i++) {
|
||||||
|
if (this.removingItems[i] === item) {
|
||||||
|
removingItems.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishAnimations() {
|
||||||
|
this.removingItems.forEach(item => item.destroyAll())
|
||||||
|
this.removingItems = [];
|
||||||
|
|
||||||
|
if (this.activeItem) {
|
||||||
|
this.activeItem.finishIfRunning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
this.removingItems.forEach(item => item.destroyAll());
|
||||||
|
this.activeItem?.destroyAll();
|
||||||
|
this.pendingItem?.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
required property Item item
|
||||||
|
property Animation activeAnimation: null
|
||||||
|
signal animationCompleted(self: SlideViewItem)
|
||||||
|
|
||||||
|
property Connections __animConnection: Connections {
|
||||||
|
target: activeAnimation
|
||||||
|
|
||||||
|
function onStopped() {
|
||||||
|
root.activeAnimation.destroy();
|
||||||
|
root.animationCompleted(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAnimation(component: Component) {
|
||||||
|
this.stopIfRunning();
|
||||||
|
this.activeAnimation = component.createObject(this, {
|
||||||
|
target: this.item
|
||||||
|
});
|
||||||
|
this.activeAnimation.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopIfRunning() {
|
||||||
|
if (this.activeAnimation) {
|
||||||
|
this.activeAnimation.stop();
|
||||||
|
this.activeAnimation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishIfRunning() {
|
||||||
|
if (this.activeAnimation) {
|
||||||
|
// animator types dont handle complete correctly.
|
||||||
|
this.activeAnimation.complete();
|
||||||
|
this.activeAnimation.stop();
|
||||||
|
this.item.x = 0;
|
||||||
|
this.item.y = 0;
|
||||||
|
this.activeAnimation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyAll() {
|
||||||
|
this.item.destroy();
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import "../"
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitWidth: 26
|
||||||
|
implicitHeight: 26
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: `../assets/${Globals.isDarkTheme ? "sun" : "moon"}.png`
|
||||||
|
smooth: false
|
||||||
|
|
||||||
|
scale: root.containsMouse ? 0.9 : 1
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: () => {
|
||||||
|
Globals.isDarkTheme = !Globals.isDarkTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Io
|
||||||
|
import ".."
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: timetxt
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
font.pointSize: 16
|
||||||
|
font.bold: true
|
||||||
|
color: "white"
|
||||||
|
opacity: 0.5
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: dateProc
|
||||||
|
command: ["date", "+%H⯁%M"]
|
||||||
|
running: true
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => timetxt.text = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
interval: 1000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: dateProc.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import "root:/"
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
required property var bar
|
||||||
|
|
||||||
|
property TooltipItem activeTooltip: null
|
||||||
|
property TooltipItem activeMenu: null
|
||||||
|
|
||||||
|
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip
|
||||||
|
property TooltipItem lastActiveItem: null
|
||||||
|
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem
|
||||||
|
property real hangTime: lastActiveItem?.hangTime ?? 0
|
||||||
|
|
||||||
|
property Item tooltipItem: null
|
||||||
|
|
||||||
|
onActiveItemChanged: {
|
||||||
|
if (activeItem != null) {
|
||||||
|
hangTimer.stop();
|
||||||
|
activeItem.targetVisible = true;
|
||||||
|
|
||||||
|
if (tooltipItem) {
|
||||||
|
activeItem.parent = tooltipItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastActiveItem != null && lastActiveItem != activeItem) {
|
||||||
|
if (activeItem != null)
|
||||||
|
lastActiveItem.targetVisible = false;
|
||||||
|
else if (root.hangTime == 0)
|
||||||
|
doLastHide();
|
||||||
|
else
|
||||||
|
hangTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeItem != null)
|
||||||
|
lastActiveItem = activeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItem(item: TooltipItem) {
|
||||||
|
if (item.isMenu) {
|
||||||
|
activeMenu = item;
|
||||||
|
} else {
|
||||||
|
activeTooltip = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(item: TooltipItem) {
|
||||||
|
if (item.isMenu && activeMenu == item) {
|
||||||
|
activeMenu = null;
|
||||||
|
} else if (!item.isMenu && activeTooltip == item) {
|
||||||
|
activeTooltip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doLastHide() {
|
||||||
|
lastActiveItem.targetVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHidden(item: TooltipItem) {
|
||||||
|
if (item == lastActiveItem) {
|
||||||
|
lastActiveItem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: hangTimer
|
||||||
|
interval: root.hangTime
|
||||||
|
onTriggered: root.doLastHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0
|
||||||
|
Behavior on scaleMul {
|
||||||
|
SmoothedAnimation {
|
||||||
|
velocity: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: popupLoader
|
||||||
|
activeAsync: root.shownItem != null
|
||||||
|
|
||||||
|
PopupWindow {
|
||||||
|
id: popup
|
||||||
|
|
||||||
|
anchor {
|
||||||
|
window: root.bar
|
||||||
|
rect.y: root.bar.tooltipYOffset
|
||||||
|
adjustment: PopupAdjustment.None
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandWindow.opacity: root.scaleMul
|
||||||
|
|
||||||
|
HyprlandWindow.visibleMask: Region {
|
||||||
|
id: visibleMask
|
||||||
|
item: tooltipItem
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
|
||||||
|
function onScaleMulChanged() {
|
||||||
|
visibleMask.changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//height: bar.height
|
||||||
|
//implicitWidth: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
|
||||||
|
//implicitHeight: Math.max(700, tooltipItem.largestAnimHeight) // max due to qtwayland glitches
|
||||||
|
visible: true
|
||||||
|
color: "transparent"
|
||||||
|
//color: "#20ff0000"
|
||||||
|
|
||||||
|
mask: Region {
|
||||||
|
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
active: activeItem?.isMenu ?? false
|
||||||
|
windows: [popup, bar, ...(activeItem?.grabWindows ?? [])]
|
||||||
|
onActiveChanged: {
|
||||||
|
if (!active && activeItem?.isMenu) {
|
||||||
|
activeMenu.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: tooltipItem
|
||||||
|
Component.onCompleted: {
|
||||||
|
root.tooltipItem = this;
|
||||||
|
if (root.shownItem) {
|
||||||
|
root.shownItem.parent = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
//highestAnimY = targetY - targetHeight / 2;
|
||||||
|
//lowestAnimY = targetY + targetHeight / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: Scale {
|
||||||
|
origin.x: 0
|
||||||
|
origin.y: tooltipItem.height / 2
|
||||||
|
xScale: 0.9 + scaleMul * 0.1
|
||||||
|
yScale: xScale
|
||||||
|
}
|
||||||
|
|
||||||
|
clip: width != targetWidth || height != targetHeight
|
||||||
|
|
||||||
|
// bkg
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#3B253F"
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var targetWidth: shownItem?.implicitWidth ?? 0
|
||||||
|
readonly property var targetHeight: shownItem?.implicitHeight ?? 0
|
||||||
|
|
||||||
|
readonly property real targetY: {
|
||||||
|
if (shownItem == null)
|
||||||
|
return 0;
|
||||||
|
const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
|
||||||
|
return bar.boundedY(target, shownItem.implicitHeight / 2);
|
||||||
|
}
|
||||||
|
readonly property real targetX: {
|
||||||
|
if (root.shownItem == null)
|
||||||
|
return 0;
|
||||||
|
const target = root.bar.contentItem.mapFromItem(root.shownItem.owner, 0, root.shownItem.targetRelativeX).x;
|
||||||
|
return root.bar.boundedX(target, root.shownItem.implicitWidth / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
property var w: -1
|
||||||
|
implicitWidth: Math.max(1, w)
|
||||||
|
|
||||||
|
property var y1: -1
|
||||||
|
property var y2: -1
|
||||||
|
|
||||||
|
y: y1 - popup.anchor.rect.y
|
||||||
|
height: y2 - y1
|
||||||
|
|
||||||
|
readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
|
||||||
|
|
||||||
|
onAnyAnimsRunningChanged: {
|
||||||
|
if (!anyAnimsRunning) {
|
||||||
|
largestAnimWidth = targetWidth;
|
||||||
|
//highestAnimY = y1;
|
||||||
|
//lowestAnimY = y2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SmoothedAnimation on y1 {
|
||||||
|
id: y1Anim
|
||||||
|
to: tooltipItem.targetY - tooltipItem.targetHeight / 2
|
||||||
|
onToChanged: {
|
||||||
|
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
|
||||||
|
stop();
|
||||||
|
tooltipItem.y1 = to;
|
||||||
|
} else {
|
||||||
|
velocity = (Math.max(tooltipItem.y1, to) - Math.min(tooltipItem.y1, to)) * 5;
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SmoothedAnimation on y2 {
|
||||||
|
id: y2Anim
|
||||||
|
to: tooltipItem.targetY + tooltipItem.targetHeight / 2
|
||||||
|
onToChanged: {
|
||||||
|
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
|
||||||
|
stop();
|
||||||
|
tooltipItem.y2 = to;
|
||||||
|
} else {
|
||||||
|
velocity = (Math.max(tooltipItem.y2, to) - Math.min(tooltipItem.y2, to)) * 5;
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SmoothedAnimation on w {
|
||||||
|
id: widthAnim
|
||||||
|
to: tooltipItem.targetWidth
|
||||||
|
onToChanged: {
|
||||||
|
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
|
||||||
|
stop();
|
||||||
|
tooltipItem.w = to;
|
||||||
|
} else {
|
||||||
|
velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5;
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import "root:/"
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
required property var bar
|
||||||
|
|
||||||
|
property TooltipItem activeTooltip: null
|
||||||
|
property TooltipItem activeMenu: null
|
||||||
|
|
||||||
|
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip
|
||||||
|
property TooltipItem lastActiveItem: null
|
||||||
|
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem
|
||||||
|
property real hangTime: lastActiveItem?.hangTime ?? 0
|
||||||
|
|
||||||
|
property Item tooltipItem: null
|
||||||
|
|
||||||
|
onActiveItemChanged: {
|
||||||
|
if (activeItem != null) {
|
||||||
|
hangTimer.stop();
|
||||||
|
activeItem.targetVisible = true;
|
||||||
|
|
||||||
|
if (tooltipItem) {
|
||||||
|
activeItem.parent = tooltipItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastActiveItem != null && lastActiveItem != activeItem) {
|
||||||
|
if (activeItem != null)
|
||||||
|
lastActiveItem.targetVisible = false;
|
||||||
|
else if (root.hangTime == 0)
|
||||||
|
doLastHide();
|
||||||
|
else
|
||||||
|
hangTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeItem != null)
|
||||||
|
lastActiveItem = activeItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItem(item: TooltipItem) {
|
||||||
|
if (item.isMenu) {
|
||||||
|
activeMenu = item;
|
||||||
|
} else {
|
||||||
|
activeTooltip = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(item: TooltipItem) {
|
||||||
|
if (item.isMenu && activeMenu == item) {
|
||||||
|
activeMenu = null;
|
||||||
|
} else if (!item.isMenu && activeTooltip == item) {
|
||||||
|
activeTooltip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doLastHide() {
|
||||||
|
lastActiveItem.targetVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHidden(item: TooltipItem) {
|
||||||
|
if (item == lastActiveItem) {
|
||||||
|
lastActiveItem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: hangTimer
|
||||||
|
interval: root.hangTime
|
||||||
|
onTriggered: doLastHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0
|
||||||
|
Behavior on scaleMul {
|
||||||
|
SmoothedAnimation {
|
||||||
|
velocity: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: popupLoader
|
||||||
|
activeAsync: shownItem != null
|
||||||
|
|
||||||
|
PopupWindow {
|
||||||
|
id: popup
|
||||||
|
|
||||||
|
anchor {
|
||||||
|
window: bar
|
||||||
|
rect.x: tooltipItem.highestAnimX
|
||||||
|
rect.y: bar.tooltipYOffset
|
||||||
|
adjustment: PopupAdjustment.None
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandWindow.opacity: root.scaleMul
|
||||||
|
|
||||||
|
HyprlandWindow.visibleMask: Region {
|
||||||
|
id: visibleMask
|
||||||
|
item: tooltipItem
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
|
||||||
|
function onScaleMulChanged() {
|
||||||
|
visibleMask.changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//height: bar.height
|
||||||
|
width: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
|
||||||
|
height: {
|
||||||
|
const h = tooltipItem.lowestAnimY - tooltipItem.highestAnimY;
|
||||||
|
//console.log(`seth ${h} ${tooltipItem.highestAnimY} ${tooltipItem.lowestAnimY}; ${tooltipItem.y1} ${tooltipItem.y2}`)
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
visible: true
|
||||||
|
color: "transparent"
|
||||||
|
//color: "#20ff0000"
|
||||||
|
|
||||||
|
mask: Region {
|
||||||
|
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
active: root.activeItem?.isMenu ?? false
|
||||||
|
windows: [popup, root.bar, ...(root.activeItem?.grabWindows ?? [])]
|
||||||
|
onActiveChanged: {
|
||||||
|
if (!active && root.activeItem?.isMenu) {
|
||||||
|
activeMenu.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Rectangle {
|
||||||
|
color: "#10ff0000"
|
||||||
|
//y: tooltipItem.highestAnimY
|
||||||
|
height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: "#1000ff00"
|
||||||
|
//y: tooltipItem.highestAnimY
|
||||||
|
height: popup.height
|
||||||
|
width: parent.width
|
||||||
|
}*/
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: tooltipItem
|
||||||
|
Component.onCompleted: {
|
||||||
|
root.tooltipItem = this;
|
||||||
|
if (root.shownItem) {
|
||||||
|
root.shownItem.parent = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
//highestAnimY = targetY - targetHeight / 2;
|
||||||
|
//lowestAnimY = targetY + targetHeight / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: Scale {
|
||||||
|
origin.x: 0
|
||||||
|
origin.y: tooltipItem.height / 2
|
||||||
|
xScale: 0.9 + scaleMul * 0.1
|
||||||
|
yScale: xScale
|
||||||
|
}
|
||||||
|
|
||||||
|
clip: width != targetWidth || height != targetHeight
|
||||||
|
|
||||||
|
// bkg
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "red"
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var targetWidth: shownItem?.implicitWidth ?? 0
|
||||||
|
readonly property var targetHeight: shownItem?.implicitHeight ?? 0
|
||||||
|
|
||||||
|
property var largestAnimWidth: 0
|
||||||
|
property var highestAnimY: 0 // unused due to reposition timing issues
|
||||||
|
property var lowestAnimY: bar.height
|
||||||
|
|
||||||
|
onTargetWidthChanged: {
|
||||||
|
if (targetWidth > largestAnimWidth) {
|
||||||
|
largestAnimWidth = targetWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTargetYChanged: updateYBounds()
|
||||||
|
onTargetHeightChanged: updateYBounds()
|
||||||
|
function updateYBounds() {
|
||||||
|
if (targetY - targetHeight / 2 < highestAnimY)
|
||||||
|
//highestAnimY = targetY - targetHeight / 2
|
||||||
|
{}
|
||||||
|
|
||||||
|
if (targetY + targetHeight / 2 > lowestAnimY)
|
||||||
|
//lowestAnimY = targetY + targetHeight / 2
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real targetY: {
|
||||||
|
if (shownItem == null)
|
||||||
|
return 0;
|
||||||
|
const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
|
||||||
|
return bar.boundedY(target, shownItem.implicitHeight / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
property var w: -1
|
||||||
|
width: Math.max(1, w)
|
||||||
|
|
||||||
|
property var y1: -1
|
||||||
|
property var y2: -1
|
||||||
|
|
||||||
|
y: y1 - popup.anchor.rect.y
|
||||||
|
height: y2 - y1
|
||||||
|
|
||||||
|
readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
|
||||||
|
|
||||||
|
onAnyAnimsRunningChanged: {
|
||||||
|
if (!anyAnimsRunning) {
|
||||||
|
largestAnimWidth = targetWidth;
|
||||||
|
//highestAnimY = y1;
|
||||||
|
//lowestAnimY = y2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SmoothedAnimation on y1 {
|
||||||
|
id: y1Anim
|
||||||
|
to: tooltipItem.targetY - tooltipItem.targetHeight / 2
|
||||||
|
onToChanged: {
|
||||||
|
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
|
||||||
|
stop();
|
||||||
|
tooltipItem.y1 = to;
|
||||||
|
} else {
|
||||||
|
velocity = (Math.max(tooltipItem.y1, to) - Math.min(tooltipItem.y1, to)) * 5;
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SmoothedAnimation on y2 {
|
||||||
|
id: y2Anim
|
||||||
|
to: tooltipItem.targetY + tooltipItem.targetHeight / 2
|
||||||
|
onToChanged: {
|
||||||
|
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
|
||||||
|
stop();
|
||||||
|
tooltipItem.y2 = to;
|
||||||
|
} else {
|
||||||
|
velocity = (Math.max(tooltipItem.y2, to) - Math.min(tooltipItem.y2, to)) * 5;
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SmoothedAnimation on w {
|
||||||
|
id: widthAnim
|
||||||
|
to: tooltipItem.targetWidth
|
||||||
|
onToChanged: {
|
||||||
|
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
|
||||||
|
stop();
|
||||||
|
tooltipItem.w = to;
|
||||||
|
} else {
|
||||||
|
velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5;
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
required property var tooltip
|
||||||
|
required property Item owner
|
||||||
|
property bool isMenu: false
|
||||||
|
property list<QtObject> grabWindows
|
||||||
|
property bool hoverable: isMenu
|
||||||
|
property bool animateSize: true
|
||||||
|
property bool show: false
|
||||||
|
property bool preloadBackground: root.visible
|
||||||
|
|
||||||
|
property real targetRelativeX: owner.width / 2
|
||||||
|
property real hangTime: isMenu ? 0 : 200
|
||||||
|
|
||||||
|
signal close
|
||||||
|
|
||||||
|
readonly property alias contentItem: contentItem
|
||||||
|
default property alias data: contentItem.data
|
||||||
|
|
||||||
|
property Component backgroundComponent: null
|
||||||
|
|
||||||
|
onShowChanged: {
|
||||||
|
if (show)
|
||||||
|
tooltip.setItem(this);
|
||||||
|
else
|
||||||
|
tooltip.removeItem(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool targetVisible: false
|
||||||
|
property real targetOpacity: 0
|
||||||
|
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
|
||||||
|
|
||||||
|
Behavior on targetOpacity {
|
||||||
|
id: opacityAnimation
|
||||||
|
SmoothedAnimation {
|
||||||
|
velocity: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapOpacity(opacity: real) {
|
||||||
|
opacityAnimation.enabled = false;
|
||||||
|
targetOpacity = opacity;
|
||||||
|
opacityAnimation.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTargetVisibleChanged: {
|
||||||
|
if (targetVisible) {
|
||||||
|
visible = true;
|
||||||
|
targetOpacity = 1;
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
targetOpacity = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTargetOpacityChanged: {
|
||||||
|
if (!targetVisible && targetOpacity == 0) {
|
||||||
|
visible = false;
|
||||||
|
this.parent = null;
|
||||||
|
if (tooltip)
|
||||||
|
tooltip.onHidden(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: false
|
||||||
|
//clip: true
|
||||||
|
implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
|
||||||
|
implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
|
||||||
|
|
||||||
|
readonly property Item item: contentItem
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.backgroundComponent && (root.visible || root.preloadBackground)
|
||||||
|
asynchronous: !root.visible && root.preloadBackground
|
||||||
|
sourceComponent: backgroundComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: contentItem
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 5
|
||||||
|
|
||||||
|
implicitHeight: children[0].implicitHeight
|
||||||
|
implicitWidth: children[0].implicitWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
required property var tooltip
|
||||||
|
required property Item owner
|
||||||
|
property bool isMenu: false
|
||||||
|
property list<QtObject> grabWindows
|
||||||
|
property bool hoverable: isMenu
|
||||||
|
property bool animateSize: true
|
||||||
|
property bool show: false
|
||||||
|
property bool preloadBackground: root.visible
|
||||||
|
|
||||||
|
property real targetRelativeY: owner.height / 2
|
||||||
|
property real hangTime: isMenu ? 0 : 200
|
||||||
|
|
||||||
|
signal close
|
||||||
|
|
||||||
|
readonly property alias contentItem: contentItem
|
||||||
|
default property alias data: contentItem.data
|
||||||
|
|
||||||
|
property Component backgroundComponent: null
|
||||||
|
|
||||||
|
onShowChanged: {
|
||||||
|
console.log(this);
|
||||||
|
if (show)
|
||||||
|
tooltip.setItem(this);
|
||||||
|
else
|
||||||
|
tooltip.removeItem(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool targetVisible: false
|
||||||
|
property real targetOpacity: 0
|
||||||
|
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
|
||||||
|
|
||||||
|
Behavior on targetOpacity {
|
||||||
|
id: opacityAnimation
|
||||||
|
SmoothedAnimation {
|
||||||
|
velocity: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapOpacity(opacity: real) {
|
||||||
|
opacityAnimation.enabled = false;
|
||||||
|
targetOpacity = opacity;
|
||||||
|
opacityAnimation.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTargetVisibleChanged: {
|
||||||
|
if (targetVisible) {
|
||||||
|
visible = true;
|
||||||
|
targetOpacity = 1;
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
targetOpacity = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTargetOpacityChanged: {
|
||||||
|
if (!targetVisible && targetOpacity == 0) {
|
||||||
|
visible = false;
|
||||||
|
this.parent = null;
|
||||||
|
if (tooltip)
|
||||||
|
tooltip.onHidden(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: false
|
||||||
|
//clip: true
|
||||||
|
implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
|
||||||
|
implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
|
||||||
|
|
||||||
|
readonly property Item item: contentItem
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.backgroundComponent && (root.visible || root.preloadBackground)
|
||||||
|
asynchronous: !root.visible && root.preloadBackground
|
||||||
|
sourceComponent: root.backgroundComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: contentItem
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 5
|
||||||
|
|
||||||
|
implicitHeight: children[0].implicitHeight
|
||||||
|
implicitWidth: children[0].implicitWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import "mpris"
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var bar
|
||||||
|
|
||||||
|
property var node: Pipewire.defaultAudioSink
|
||||||
|
PwObjectTracker {
|
||||||
|
objects: [root.node]
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: 26
|
||||||
|
implicitHeight: 26
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
|
||||||
|
//enabled: MprisController.canChangeVolume
|
||||||
|
|
||||||
|
opacity: enabled ? 1 : 0.5
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: `../assets/${root.node.audio.muted ? "noaudio" : "audio"}.png`
|
||||||
|
smooth: false
|
||||||
|
|
||||||
|
scale: popupLoader.active ? 0.9 : 1
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: node.audio.muted = !node.audio.muted
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: popupLoader
|
||||||
|
|
||||||
|
property bool popupContainsMouse
|
||||||
|
active: root.enabled && (root.containsMouse || popupContainsMouse)
|
||||||
|
Behavior on active {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PopupWindow {
|
||||||
|
id: popup
|
||||||
|
|
||||||
|
visible: true
|
||||||
|
anchor.window: root.bar
|
||||||
|
width: 200
|
||||||
|
height: 32
|
||||||
|
color: "#9B5B36"
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onXChanged() {
|
||||||
|
popup.updatePos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component.onCompleted: updatePos()
|
||||||
|
function updatePos() {
|
||||||
|
const pos = root.mapToGlobal(root.width / 2 - popup.width / 2, root.height);
|
||||||
|
popup.anchor.rect.x = pos.x;
|
||||||
|
popup.anchor.rect.y = pos.y + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider {
|
||||||
|
id: slider
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
anchors.margins: 5
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
|
||||||
|
value: root.node.audio.volume
|
||||||
|
onValueChanged: root.node.audio.volume = value
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
x: slider.leftPadding
|
||||||
|
y: slider.topPadding + slider.availableHeight / 2 - height / 2
|
||||||
|
implicitHeight: 16
|
||||||
|
width: slider.availableWidth
|
||||||
|
height: implicitHeight
|
||||||
|
color: "#824524"
|
||||||
|
opacity: node.audio.muted ? 0.5 : 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.margins: 5
|
||||||
|
x: anchors.leftMargin
|
||||||
|
y: anchors.topMargin
|
||||||
|
width: slider.visualPosition * (parent.width - anchors.leftMargin - anchors.rightMargin)
|
||||||
|
height: parent.height - anchors.topMargin - anchors.bottomMargin
|
||||||
|
color: "#8D804B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle: Rectangle {
|
||||||
|
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
|
||||||
|
y: slider.topPadding + slider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: 12
|
||||||
|
implicitHeight: 12
|
||||||
|
rotation: 45
|
||||||
|
color: "#8D804B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: popupMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
propagateComposedEvents: true
|
||||||
|
onEntered: popupLoader.popupContainsMouse = true
|
||||||
|
onExited: popupLoader.popupContainsMouse = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var bar
|
||||||
|
property int wsBaseIndex: 1
|
||||||
|
property int wsCount: 7
|
||||||
|
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen)
|
||||||
|
property int currentIndex: 0
|
||||||
|
property int existsCount: 0
|
||||||
|
|
||||||
|
signal workspaceAdded(workspace: HyprlandWorkspace)
|
||||||
|
|
||||||
|
implicitWidth: row.implicitWidth
|
||||||
|
implicitHeight: row.implicitHeight
|
||||||
|
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
|
||||||
|
onWheel: e => {
|
||||||
|
e.accepted = true;
|
||||||
|
const step = -Math.sign(e.angleDelta.y);
|
||||||
|
const targetWs = currentIndex + step;
|
||||||
|
|
||||||
|
if (targetWs >= wsBaseIndex && targetWs < wsBaseIndex + wsCount) {
|
||||||
|
Hyprland.dispatch(`workspace ${targetWs}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: row
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.wsCount
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: wsItem
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
property int wsIndex: root.wsBaseIndex + index
|
||||||
|
property HyprlandWorkspace workspace: null
|
||||||
|
property bool exists: workspace != null
|
||||||
|
property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace
|
||||||
|
|
||||||
|
implicitWidth: 26
|
||||||
|
implicitHeight: 26
|
||||||
|
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
|
||||||
|
onPressed: Hyprland.dispatch(`workspace ${wsIndex}`)
|
||||||
|
|
||||||
|
onExistsChanged: {
|
||||||
|
root.existsCount += exists ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active)
|
||||||
|
root.currentIndex = wsIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
|
||||||
|
function onWorkspaceAdded(workspace: HyprlandWorkspace) {
|
||||||
|
if (workspace.id == wsItem.wsIndex) {
|
||||||
|
wsItem.workspace = workspace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: fill
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
radius: width / 2
|
||||||
|
color: wsItem.exists ? "#C0DB4C" : "#59622D"
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
smooth: false
|
||||||
|
source: "../assets/workspace.png"
|
||||||
|
}
|
||||||
|
MultiEffect {
|
||||||
|
source: fill
|
||||||
|
anchors.fill: fill
|
||||||
|
blur: 1.0
|
||||||
|
brightness: 0.3
|
||||||
|
blurEnabled: true
|
||||||
|
visible: wsItem.active
|
||||||
|
opacity: wsItem.active ? 1.0 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Hyprland.workspaces
|
||||||
|
|
||||||
|
function onObjectInsertedPost(workspace) {
|
||||||
|
root.workspaceAdded(workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Hyprland.workspaces.values.forEach(workspace => {
|
||||||
|
root.workspaceAdded(workspace);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQml.Models
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
property MprisPlayer trackedPlayer: null
|
||||||
|
property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null
|
||||||
|
signal trackChanged(reverse: bool)
|
||||||
|
|
||||||
|
property bool __reverse: false
|
||||||
|
|
||||||
|
property var activeTrack
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: Mpris.players
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
required property MprisPlayer modelData
|
||||||
|
target: modelData
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (root.trackedPlayer == null || modelData.isPlaying) {
|
||||||
|
root.trackedPlayer = modelData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
|
||||||
|
for (const player of Mpris.players.values) {
|
||||||
|
if (player.playbackState.isPlaying) {
|
||||||
|
root.trackedPlayer = player;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackedPlayer == null && Mpris.players.values.length != 0) {
|
||||||
|
trackedPlayer = Mpris.players.values[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlaybackStateChanged() {
|
||||||
|
if (root.trackedPlayer !== modelData)
|
||||||
|
root.trackedPlayer = modelData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: activePlayer
|
||||||
|
|
||||||
|
function onPostTrackChanged() {
|
||||||
|
root.updateTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTrackArtUrlChanged() {
|
||||||
|
//root.updateTrack();
|
||||||
|
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
|
||||||
|
// cantata likes to send cover updates *BEFORE* updating the track info.
|
||||||
|
// as such, art url changes shouldn't be able to break the reverse animation
|
||||||
|
const r = root.__reverse;
|
||||||
|
root.updateTrack();
|
||||||
|
root.__reverse = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivePlayerChanged: this.updateTrack()
|
||||||
|
|
||||||
|
function updateTrack() {
|
||||||
|
//console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
|
||||||
|
this.activeTrack = {
|
||||||
|
uniqueId: this.activePlayer?.uniqueId ?? 0,
|
||||||
|
artUrl: this.activePlayer?.trackArtUrl ?? "",
|
||||||
|
title: this.activePlayer?.trackTitle || "Unknown Title",
|
||||||
|
artist: this.activePlayer?.trackArtist || "Unknown Artist",
|
||||||
|
album: this.activePlayer?.trackAlbum || "Unknown Album"
|
||||||
|
};
|
||||||
|
|
||||||
|
this.trackChanged(__reverse);
|
||||||
|
this.__reverse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying
|
||||||
|
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false
|
||||||
|
function togglePlaying() {
|
||||||
|
if (this.canTogglePlaying)
|
||||||
|
this.activePlayer.togglePlaying();
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false
|
||||||
|
function previous() {
|
||||||
|
if (this.canGoPrevious) {
|
||||||
|
this.__reverse = true;
|
||||||
|
this.activePlayer.previous();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool canGoNext: this.activePlayer?.canGoNext ?? false
|
||||||
|
function next() {
|
||||||
|
if (this.canGoNext) {
|
||||||
|
this.__reverse = false;
|
||||||
|
this.activePlayer.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl
|
||||||
|
|
||||||
|
property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl
|
||||||
|
property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None
|
||||||
|
function setLoopState(loopState: var) {
|
||||||
|
if (this.loopSupported) {
|
||||||
|
this.activePlayer.loopState = loopState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl
|
||||||
|
property bool hasShuffle: this.activePlayer?.shuffle ?? false
|
||||||
|
function setShuffle(shuffle: bool) {
|
||||||
|
if (this.shuffleSupported) {
|
||||||
|
this.activePlayer.shuffle = shuffle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActivePlayer(player: MprisPlayer) {
|
||||||
|
const targetPlayer = player ?? Mpris.players[0];
|
||||||
|
console.log(`setactive: ${targetPlayer} from ${activePlayer}`);
|
||||||
|
|
||||||
|
if (targetPlayer && this.activePlayer) {
|
||||||
|
this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer);
|
||||||
|
} else {
|
||||||
|
// always animate forward if going to null
|
||||||
|
this.__reverse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trackedPlayer = targetPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "mpris"
|
||||||
|
|
||||||
|
function pauseAll(): void {
|
||||||
|
for (const player of Mpris.players.values) {
|
||||||
|
if (player.canPause)
|
||||||
|
player.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPause(): void {
|
||||||
|
root.togglePlaying();
|
||||||
|
}
|
||||||
|
function previous(): void {
|
||||||
|
root.previous();
|
||||||
|
}
|
||||||
|
function next(): void {
|
||||||
|
root.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "../.."
|
||||||
|
import ".."
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var window
|
||||||
|
required property var bar
|
||||||
|
|
||||||
|
readonly property var activePlayer: MprisController.activePlayer
|
||||||
|
|
||||||
|
property var isPopupOpen: false
|
||||||
|
|
||||||
|
implicitWidth: column.implicitWidth + 10
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onClicked: isPopupOpen = !isPopupOpen
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: widget
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
scale: root.isPopupOpen ? 0.92 : 1
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: column.implicitHeight + 10
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: column
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableIcon {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
implicitWidth: 20
|
||||||
|
implicitHeight: 20
|
||||||
|
image: "../../assets/fastforward.png"
|
||||||
|
mirror: true
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: MprisController.canGoPrevious
|
||||||
|
onClicked: MprisController.previous()
|
||||||
|
scale: containsMouse ? 0.9 : 1
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableIcon {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
implicitWidth: 26
|
||||||
|
implicitHeight: 26
|
||||||
|
Layout.leftMargin: -4
|
||||||
|
Layout.rightMargin: -4
|
||||||
|
image: `../../assets/${MprisController.isPlaying ? "pause" : "play"}.png`
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: MprisController.canTogglePlaying
|
||||||
|
onClicked: MprisController.togglePlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableIcon {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
implicitWidth: 20
|
||||||
|
implicitHeight: 20
|
||||||
|
image: "../../assets/fastforward.png"
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: MprisController.canGoNext
|
||||||
|
onClicked: MprisController.next()
|
||||||
|
scale: containsMouse ? 0.9 : 1
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onIsPopupOpenChanged() {
|
||||||
|
root.window.focusable = root.isPopupOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyLoader {
|
||||||
|
id: popupLoader
|
||||||
|
active: root.isPopupOpen
|
||||||
|
// Leave time for exit anim
|
||||||
|
Behavior on active {
|
||||||
|
enabled: popupLoader.active
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayersPopup {
|
||||||
|
id: popup
|
||||||
|
visible: true
|
||||||
|
isOpen: root.isPopupOpen
|
||||||
|
|
||||||
|
anchor.window: root.window
|
||||||
|
anchor.rect.x: root.window.width
|
||||||
|
anchor.rect.y: 0
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
active: root.isPopupOpen
|
||||||
|
windows: [popup]
|
||||||
|
}
|
||||||
|
contentItem {
|
||||||
|
focus: true
|
||||||
|
Keys.onEscapePressed: root.isPopupOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component CenteredText: Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
property alias text: label.text
|
||||||
|
property alias font: label.font
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: label
|
||||||
|
visible: text != ""
|
||||||
|
anchors.centerIn: parent
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: Math.min(parent.width - 20, implicitWidth)
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "../.."
|
||||||
|
import "../../components"
|
||||||
|
import ".."
|
||||||
|
|
||||||
|
PopupWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool isOpen: false
|
||||||
|
|
||||||
|
implicitWidth: 700
|
||||||
|
//implicitHeight: popupContent.implicitHeight + popupContent.anchors.margins * 2
|
||||||
|
implicitHeight: 800
|
||||||
|
|
||||||
|
mask: Region {
|
||||||
|
item: wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
property Scope positionInfo: Scope {
|
||||||
|
id: positionInfo
|
||||||
|
|
||||||
|
property int position: Math.floor(MprisController.activePlayer.position)
|
||||||
|
property int length: Math.floor(MprisController.activePlayer.length)
|
||||||
|
|
||||||
|
FrameAnimation {
|
||||||
|
id: posTracker
|
||||||
|
running: MprisController.activePlayer.isPlaying
|
||||||
|
onTriggered: MprisController.activePlayer.positionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeStr(time: int): string {
|
||||||
|
const seconds = time % 60;
|
||||||
|
const minutes = Math.floor(time / 60);
|
||||||
|
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rope {
|
||||||
|
segmentCount: 4
|
||||||
|
segmentLen: 22
|
||||||
|
start: Qt.vector2d(250, -5)
|
||||||
|
end: Qt.vector2d(wrapper.x + 49, wrapper.y)
|
||||||
|
}
|
||||||
|
Rope {
|
||||||
|
segmentCount: 4
|
||||||
|
segmentLen: 22
|
||||||
|
start: Qt.vector2d(wrapper.width + 200 - 50, -5)
|
||||||
|
end: Qt.vector2d(wrapper.width + wrapper.x - 49, wrapper.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: wrapper
|
||||||
|
implicitWidth: 500
|
||||||
|
implicitHeight: popupContent.implicitHeight + popupContent.anchors.margins * 2
|
||||||
|
color: "#824524"
|
||||||
|
|
||||||
|
x: 200
|
||||||
|
y: -wrapper.height
|
||||||
|
readonly property var targetX: 200
|
||||||
|
readonly property var targetY: root.isOpen ? 70 : -wrapper.height - 50
|
||||||
|
property var velocityX: 0
|
||||||
|
property var velocityY: 0
|
||||||
|
|
||||||
|
FrameAnimation {
|
||||||
|
running: true
|
||||||
|
function dampingVelocity(currentVelocity, delta) {
|
||||||
|
const spring = 8.0;
|
||||||
|
const damping = 0.3;
|
||||||
|
const springForce = spring * delta;
|
||||||
|
const dampingForce = -damping * currentVelocity;
|
||||||
|
return currentVelocity + (springForce + dampingForce);
|
||||||
|
}
|
||||||
|
onTriggered: {
|
||||||
|
const deltaX = wrapper.targetX - wrapper.x;
|
||||||
|
const deltaY = wrapper.targetY - wrapper.y;
|
||||||
|
if (Math.abs(deltaX) > 0.1 || Math.abs(deltaY) > 0.1) {
|
||||||
|
wrapper.velocityX = dampingVelocity(wrapper.velocityX, deltaX);
|
||||||
|
wrapper.velocityY = dampingVelocity(wrapper.velocityY, deltaY);
|
||||||
|
wrapper.x += wrapper.velocityX * frameTime;
|
||||||
|
wrapper.y += wrapper.velocityY * frameTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
property var prevMouseX: 0
|
||||||
|
property var prevMouseY: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
onPressed: e => {
|
||||||
|
prevMouseX = e.x;
|
||||||
|
prevMouseY = e.y;
|
||||||
|
}
|
||||||
|
onPositionChanged: e => {
|
||||||
|
wrapper.x += (e.x - prevMouseX) * 0.3;
|
||||||
|
wrapper.y += (e.y - prevMouseY) * 0.3;
|
||||||
|
prevMouseX = e.x;
|
||||||
|
prevMouseY = e.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Image {
|
||||||
|
scale: 10
|
||||||
|
anchors.fill: parent
|
||||||
|
source: "../../assets/wood.png"
|
||||||
|
smooth: false
|
||||||
|
fillMode: Image.Tile
|
||||||
|
opacity: 0.4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: popupContent
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: MprisController
|
||||||
|
|
||||||
|
function onTrackChanged(reverse: bool) {
|
||||||
|
trackStack.updateTrack(reverse, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: playerSelectorContainment
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: playerSelector.implicitHeight
|
||||||
|
implicitWidth: playerSelector.implicitWidth
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitWidth: 50
|
||||||
|
implicitHeight: 50
|
||||||
|
color: "#824524"
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: playerSelector
|
||||||
|
property Item selectedPlayerDisplay: Mpris.players[0]
|
||||||
|
x: parent.width / 2 - (selectedPlayerDisplay ? selectedPlayerDisplay.x + selectedPlayerDisplay.width / 2 : 0)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 400
|
||||||
|
easing.type: Easing.OutExpo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Mpris.players
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
required property MprisPlayer modelData
|
||||||
|
readonly property bool selected: modelData == MprisController.activePlayer
|
||||||
|
onSelectedChanged: () => {
|
||||||
|
if (selected) {
|
||||||
|
playerSelector.selectedPlayerDisplay = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: childrenRect.width
|
||||||
|
implicitHeight: childrenRect.height
|
||||||
|
|
||||||
|
onClicked: MprisController.setActivePlayer(modelData)
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 50
|
||||||
|
height: 50
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
source: {
|
||||||
|
// So janky xD
|
||||||
|
const identity = modelData.identity.toLowerCase();
|
||||||
|
|
||||||
|
if (identity.includes("chrome") || identity.includes("chromium")) {
|
||||||
|
return Quickshell.iconPath("google-chrome") || Quickshell.iconPath("chromium");
|
||||||
|
}
|
||||||
|
if (identity.includes("firefox")) {
|
||||||
|
return Quickshell.iconPath("firefox");
|
||||||
|
}
|
||||||
|
if (identity.includes("spotify")) {
|
||||||
|
return Quickshell.iconPath("spotify");
|
||||||
|
}
|
||||||
|
if (modelData.identity == "Brave") {
|
||||||
|
return "/opt/brave-bin/product_logo_64.png";
|
||||||
|
}
|
||||||
|
const entry = DesktopEntries.byId(modelData.desktopEntry);
|
||||||
|
if (entry && entry.icon) {
|
||||||
|
return Quickshell.iconPath(entry.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "../../assets/mpd.png";
|
||||||
|
}
|
||||||
|
smooth: false
|
||||||
|
sourceSize.width: 50
|
||||||
|
sourceSize.height: 50
|
||||||
|
cache: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 16
|
||||||
|
Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: MprisController.activePlayer.identity
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SlideView {
|
||||||
|
id: trackStack
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 100
|
||||||
|
clip: animating || (lastFlicked?.contentX ?? 0) != 0
|
||||||
|
|
||||||
|
property Flickable lastFlicked
|
||||||
|
property bool reverse: false
|
||||||
|
|
||||||
|
Component.onCompleted: updateTrack(false, true)
|
||||||
|
|
||||||
|
function updateTrack(reverse: bool, immediate: bool) {
|
||||||
|
this.reverse = reverse;
|
||||||
|
this.replace(trackComponent, {
|
||||||
|
track: MprisController.activeTrack
|
||||||
|
}, immediate);
|
||||||
|
}
|
||||||
|
|
||||||
|
property var trackComponent: Component {
|
||||||
|
Flickable {
|
||||||
|
id: flickable
|
||||||
|
required property var track
|
||||||
|
readonly property bool svReady: img.status === Image.Ready
|
||||||
|
contentWidth: width + 1
|
||||||
|
onDragStarted: trackStack.lastFlicked = this
|
||||||
|
onDragEnded: {
|
||||||
|
if (Math.abs(contentX) > 75) {
|
||||||
|
if (contentX < 0)
|
||||||
|
MprisController.previous();
|
||||||
|
else if (contentX > 0)
|
||||||
|
MprisController.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
|
||||||
|
implicitHeight: 128
|
||||||
|
implicitWidth: 128
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: img
|
||||||
|
anchors.fill: parent
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
visible: flickable.track.artUrl != ""
|
||||||
|
source: flickable.track.artUrl
|
||||||
|
cache: false
|
||||||
|
asynchronous: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: flickable.track.title
|
||||||
|
font.pointSize: albumLabel.font.pointSize + 1
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
color: "white"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.maximumWidth: Math.min(300, implicitWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: albumLabel
|
||||||
|
text: flickable.track.album
|
||||||
|
opacity: 0.8
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
color: "white"
|
||||||
|
Layout.maximumWidth: Math.min(300, implicitWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: flickable.track.artist
|
||||||
|
opacity: 0.8
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
color: "white"
|
||||||
|
Layout.maximumWidth: Math.min(300, implicitWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real fromPos: trackStack.width * (trackStack.reverse ? -1 : 1)
|
||||||
|
|
||||||
|
enterTransition: PropertyAnimation {
|
||||||
|
property: "x"
|
||||||
|
from: trackStack.fromPos
|
||||||
|
to: 0
|
||||||
|
duration: 350
|
||||||
|
easing.type: Easing.OutExpo
|
||||||
|
}
|
||||||
|
exitTransition: PropertyAnimation {
|
||||||
|
property: "x"
|
||||||
|
to: target.x - trackStack.fromPos
|
||||||
|
duration: 350
|
||||||
|
easing.type: Easing.OutExpo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: controlsRow.implicitHeight
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: controlsRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
ClickableIcon {
|
||||||
|
Layout.leftMargin: 2
|
||||||
|
Layout.rightMargin: 2
|
||||||
|
implicitWidth: 24
|
||||||
|
implicitHeight: 24
|
||||||
|
enabled: MprisController.loopSupported
|
||||||
|
image: {
|
||||||
|
switch (MprisController.loopState) {
|
||||||
|
case MprisLoopState.None:
|
||||||
|
return "../../assets/repeat-off.png";
|
||||||
|
case MprisLoopState.Playlist:
|
||||||
|
return "../../assets/repeat.png";
|
||||||
|
case MprisLoopState.Track:
|
||||||
|
return "../../assets/repeat-once.png";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClicked: {
|
||||||
|
let target = MprisLoopState.None;
|
||||||
|
switch (MprisController.loopState) {
|
||||||
|
case MprisLoopState.None:
|
||||||
|
target = MprisLoopState.Playlist;
|
||||||
|
break;
|
||||||
|
case MprisLoopState.Playlist:
|
||||||
|
target = MprisLoopState.Track;
|
||||||
|
break;
|
||||||
|
case MprisLoopState.Track:
|
||||||
|
target = MprisLoopState.None;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
MprisController.setLoopState(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableIcon {
|
||||||
|
Layout.leftMargin: 2
|
||||||
|
Layout.rightMargin: 2
|
||||||
|
implicitWidth: 32
|
||||||
|
implicitHeight: 32
|
||||||
|
enabled: MprisController.canGoPrevious
|
||||||
|
image: "../../assets/fastforward.png"
|
||||||
|
mirror: true
|
||||||
|
onClicked: MprisController.previous()
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableIcon {
|
||||||
|
implicitWidth: 42
|
||||||
|
implicitHeight: 42
|
||||||
|
enabled: MprisController.canTogglePlaying
|
||||||
|
image: `../../assets/${MprisController.isPlaying ? "pause" : "play"}.png`
|
||||||
|
onClicked: MprisController.togglePlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableIcon {
|
||||||
|
Layout.leftMargin: 2
|
||||||
|
Layout.rightMargin: 2
|
||||||
|
implicitWidth: 32
|
||||||
|
implicitHeight: 32
|
||||||
|
enabled: MprisController.canGoNext
|
||||||
|
image: "../../assets/fastforward.png"
|
||||||
|
onClicked: MprisController.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableIcon {
|
||||||
|
Layout.leftMargin: 2
|
||||||
|
Layout.rightMargin: 2
|
||||||
|
implicitWidth: 24
|
||||||
|
implicitHeight: 24
|
||||||
|
enabled: MprisController.shuffleSupported
|
||||||
|
image: `../../assets/${MprisController.hasShuffle ? "shuffle" : "noshuffle"}.png`
|
||||||
|
onClicked: MprisController.setShuffle(!MprisController.hasShuffle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.margins: 5
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.preferredWidth: lengthLabel.implicitWidth
|
||||||
|
text: positionInfo.timeStr(positionInfo.position)
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider {
|
||||||
|
id: slider
|
||||||
|
|
||||||
|
property bool bindSlider: true
|
||||||
|
|
||||||
|
property real boundAnimStart: 0
|
||||||
|
property real boundAnimFactor: 1
|
||||||
|
property real lastPosition: 0
|
||||||
|
property real lastLength: 0
|
||||||
|
property real boundPosition: {
|
||||||
|
const ppos = MprisController.activePlayer.position / MprisController.activePlayer.length;
|
||||||
|
const bpos = boundAnimStart;
|
||||||
|
return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor));
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: MprisController.activePlayer.canSeek
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
x: slider.leftPadding
|
||||||
|
y: slider.topPadding + slider.availableHeight / 2 - height / 2
|
||||||
|
implicitHeight: 24
|
||||||
|
width: slider.availableWidth
|
||||||
|
height: implicitHeight
|
||||||
|
color: "#824524"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.margins: 8
|
||||||
|
x: anchors.leftMargin
|
||||||
|
y: anchors.topMargin
|
||||||
|
width: slider.visualPosition * (parent.width - anchors.leftMargin - anchors.rightMargin)
|
||||||
|
height: parent.height - anchors.topMargin - anchors.bottomMargin
|
||||||
|
color: "#8D804B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle: Rectangle {
|
||||||
|
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
|
||||||
|
y: slider.topPadding + slider.availableHeight / 2 - height / 2
|
||||||
|
implicitWidth: 16
|
||||||
|
implicitHeight: 16
|
||||||
|
rotation: 45
|
||||||
|
color: "#8D804B"
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: MprisController.activePlayer
|
||||||
|
|
||||||
|
function onPositionChanged() {
|
||||||
|
slider.lastPosition = MprisController.activePlayer.position;
|
||||||
|
slider.lastLength = MprisController.activePlayer.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressedChanged: () => {
|
||||||
|
if (!pressed)
|
||||||
|
MprisController.activePlayer.position = value * MprisController.activePlayer.length;
|
||||||
|
bindSlider = !pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
when: slider.bindSlider
|
||||||
|
slider.value: slider.boundPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: lengthLabel
|
||||||
|
text: positionInfo.timeStr(positionInfo.length)
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
source: "../../assets/rope-tie.png"
|
||||||
|
smooth: false
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
x: wrapper.x + 49 - width / 2
|
||||||
|
y: wrapper.y - height / 2
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
source: "../../assets/rope-tie.png"
|
||||||
|
smooth: false
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
x: wrapper.width + wrapper.x - 49 - width / 2
|
||||||
|
y: wrapper.y - height / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import "../.."
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property var checkState: Qt.Unchecked
|
||||||
|
implicitHeight: 18
|
||||||
|
implicitWidth: 18
|
||||||
|
radius: 3
|
||||||
|
color: checkState == Qt.Checked ? "#C5E3EE" : "#90ACAD"
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
visible: checkState == Qt.Checked
|
||||||
|
anchors.fill: parent
|
||||||
|
layer.enabled: true
|
||||||
|
layer.samples: 10
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
strokeWidth: 2
|
||||||
|
capStyle: ShapePath.RoundCap
|
||||||
|
joinStyle: ShapePath.RoundJoin
|
||||||
|
fillColor: "transparent"
|
||||||
|
|
||||||
|
startX: start.x
|
||||||
|
startY: start.y
|
||||||
|
|
||||||
|
PathLine {
|
||||||
|
id: start
|
||||||
|
x: width * 0.8
|
||||||
|
y: height * 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
PathLine {
|
||||||
|
x: width * 0.35
|
||||||
|
y: height * 0.8
|
||||||
|
}
|
||||||
|
|
||||||
|
PathLine {
|
||||||
|
x: width * 0.2
|
||||||
|
y: height * 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property bool expanded: false;
|
||||||
|
|
||||||
|
readonly property bool open: progress != 0;
|
||||||
|
readonly property bool animating: internalProgress != (expanded ? 101 : -1);
|
||||||
|
|
||||||
|
implicitHeight: 16
|
||||||
|
implicitWidth: 16
|
||||||
|
property var xStart: Math.round(width * 0.3)
|
||||||
|
property var yStart: Math.round(height * 0.1)
|
||||||
|
property var xEnd: Math.round(width * 0.7)
|
||||||
|
property var yEnd: Math.round(height * 0.9)
|
||||||
|
|
||||||
|
property real internalProgress: expanded ? 101 : -1;
|
||||||
|
Behavior on internalProgress { SmoothedAnimation { velocity: 300 } }
|
||||||
|
|
||||||
|
EasingCurve {
|
||||||
|
id: curve
|
||||||
|
curve.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real progress: curve.valueAt(Math.min(100, Math.max(internalProgress, 0)) * 0.01)
|
||||||
|
|
||||||
|
rotation: progress * 90;
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.samples: 3
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
strokeWidth: 2
|
||||||
|
capStyle: ShapePath.RoundCap
|
||||||
|
joinStyle: ShapePath.MiterJoin
|
||||||
|
fillColor: "transparent"
|
||||||
|
|
||||||
|
startX: xStart
|
||||||
|
startY: yStart
|
||||||
|
|
||||||
|
PathLine {
|
||||||
|
x: xEnd
|
||||||
|
y: height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
PathLine {
|
||||||
|
y: yEnd
|
||||||
|
x: xStart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.DBusMenu
|
||||||
|
import "../.."
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
required property QsMenuEntry entry
|
||||||
|
property alias expanded: childrenRevealer.expanded
|
||||||
|
property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false)
|
||||||
|
// appears it won't actually create the handler when only used from MenuItemList.
|
||||||
|
onExpandedChanged: {}
|
||||||
|
onAnimatingChanged: {}
|
||||||
|
|
||||||
|
signal close
|
||||||
|
|
||||||
|
implicitWidth: row.implicitWidth + 4
|
||||||
|
implicitHeight: row.implicitHeight + 4
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
if (entry.hasChildren)
|
||||||
|
childrenRevealer.expanded = !childrenRevealer.expanded;
|
||||||
|
else {
|
||||||
|
entry.triggered();
|
||||||
|
if (entry.toggleType == ToggleButtonType.None)
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.containsMouse || childrenRevealer.expanded
|
||||||
|
|
||||||
|
color: "#824524"
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
id: row
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: innerRow
|
||||||
|
|
||||||
|
Item {
|
||||||
|
implicitWidth: 22
|
||||||
|
implicitHeight: 22
|
||||||
|
|
||||||
|
MenuCheckBox {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: entry.buttonType == QsMenuButtonType.CheckBox
|
||||||
|
checkState: entry.checkState
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuRadioButton {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: entry.buttonType == QsMenuButtonType.RadioButton
|
||||||
|
checkState: entry.checkState
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuChildrenRevealer {
|
||||||
|
id: childrenRevealer
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: entry.hasChildren
|
||||||
|
onOpenChanged: entry.showChildren = open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: entry.text
|
||||||
|
color: entry.enabled ? "white" : "#bbbbbb"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitWidth: 22
|
||||||
|
implicitHeight: 22
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
source: entry.icon
|
||||||
|
visible: source != ""
|
||||||
|
implicitSize: parent.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: childMenuLoader
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0
|
||||||
|
|
||||||
|
readonly property real widthDifference: {
|
||||||
|
Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth);
|
||||||
|
}
|
||||||
|
Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0
|
||||||
|
|
||||||
|
active: root.expanded || root.animating
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
sourceComponent: MenuView {
|
||||||
|
id: childrenList
|
||||||
|
menu: entry
|
||||||
|
onClose: root.close()
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import QtQuick
|
||||||
|
import "../.."
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property var checkState: Qt.Unchecked
|
||||||
|
implicitHeight: 18
|
||||||
|
implicitWidth: 18
|
||||||
|
radius: width / 2
|
||||||
|
color: checkState == Qt.Checked ? "#C5E3EE" : "#90ACAD"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: parent.width * 0.25
|
||||||
|
y: parent.height * 0.25
|
||||||
|
visible: checkState == Qt.Checked
|
||||||
|
width: parent.width * 0.5
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
property alias menu: menuView.menu
|
||||||
|
property Item animatingItem: null
|
||||||
|
property bool animating: animatingItem != null
|
||||||
|
|
||||||
|
signal close
|
||||||
|
signal submenuExpanded(item: var)
|
||||||
|
|
||||||
|
QsMenuOpener {
|
||||||
|
id: menuView
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: menuView.children
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
property var item: Component {
|
||||||
|
BoundComponent {
|
||||||
|
id: itemComponent
|
||||||
|
source: "MenuItem.qml"
|
||||||
|
|
||||||
|
property var entry: modelData
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExpandedChanged() {
|
||||||
|
if (item.expanded)
|
||||||
|
root.submenuExpanded(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAnimatingChanged() {
|
||||||
|
if (item.animating) {
|
||||||
|
root.animatingItem = this;
|
||||||
|
} else if (root.animatingItem == this) {
|
||||||
|
root.animatingItem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
|
||||||
|
function onSubmenuExpanded(expandedItem) {
|
||||||
|
if (item != expandedItem)
|
||||||
|
item.expanded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var separator: Component {
|
||||||
|
Rectangle {
|
||||||
|
color: "#824524"
|
||||||
|
implicitHeight: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceComponent: modelData.isSeparator ? separator : item
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
import ".."
|
||||||
|
import "../.."
|
||||||
|
import "../../components"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
required property var bar
|
||||||
|
implicitWidth: row.implicitWidth + 10
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: row
|
||||||
|
implicitWidth: childrenRect.width
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
margins: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: SystemTray.items
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: item
|
||||||
|
required property SystemTrayItem modelData
|
||||||
|
|
||||||
|
property bool targetMenuOpen: false
|
||||||
|
|
||||||
|
Layout.fillHeight: true
|
||||||
|
implicitWidth: height
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
width: height
|
||||||
|
source: item.modelData.icon
|
||||||
|
scale: item.targetMenuOpen ? 0.8 : 1
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
|
||||||
|
// showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton)
|
||||||
|
// fillWindowWidth: true
|
||||||
|
// extraVerticalMargin: row.spacing / 2
|
||||||
|
|
||||||
|
onClicked: event => {
|
||||||
|
event.accepted = true;
|
||||||
|
|
||||||
|
if (event.button == Qt.LeftButton) {
|
||||||
|
item.modelData.activate();
|
||||||
|
} else if (event.button == Qt.MiddleButton) {
|
||||||
|
item.modelData.secondaryActivate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: event => {
|
||||||
|
if (event.button == Qt.RightButton && item.modelData.hasMenu) {
|
||||||
|
item.targetMenuOpen = !item.targetMenuOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheel: event => {
|
||||||
|
event.accepted = true;
|
||||||
|
const points = event.angleDelta.y / 120;
|
||||||
|
item.modelData.scroll(points, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// property var tooltip: TooltipItem {
|
||||||
|
// tooltip: root.bar.tooltip
|
||||||
|
// owner: mouseArea
|
||||||
|
//
|
||||||
|
// show: mouseArea.containsMouse
|
||||||
|
//
|
||||||
|
// Text {
|
||||||
|
// id: tooltipText
|
||||||
|
// text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id
|
||||||
|
// color: "white"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
active: item.targetMenuOpen
|
||||||
|
|
||||||
|
PopupWindow {
|
||||||
|
id: rightclickMenu
|
||||||
|
anchor.window: root.bar
|
||||||
|
visible: true
|
||||||
|
|
||||||
|
Component.onCompleted: () => {
|
||||||
|
root.bar.focusable = true;
|
||||||
|
const pos = mouseArea.mapToGlobal(mouseArea.x, mouseArea.y);
|
||||||
|
anchor.rect.x = pos.x;
|
||||||
|
anchor.rect.y = pos.y + mouseArea.height;
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: item
|
||||||
|
function onTargetMenuOpenChanged() {
|
||||||
|
root.bar.focusable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentItem {
|
||||||
|
focus: true
|
||||||
|
Keys.onEscapePressed: item.targetMenuOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: Math.max(1, menuView.implicitWidth)
|
||||||
|
implicitHeight: Math.max(1, menuView.implicitHeight)
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#9B5B36"
|
||||||
|
MenuView {
|
||||||
|
id: menuView
|
||||||
|
anchors.fill: parent
|
||||||
|
menu: item.modelData.menu
|
||||||
|
onClose: item.targetMenuOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property vector2d start
|
||||||
|
required property vector2d end
|
||||||
|
|
||||||
|
property double segmentCount: 8
|
||||||
|
property double segmentLen: 50
|
||||||
|
|
||||||
|
property alias color: path.strokeColor
|
||||||
|
|
||||||
|
readonly property Component p: PathLine {
|
||||||
|
property vector2d pos
|
||||||
|
property vector2d prevPos: pos
|
||||||
|
property vector2d acc
|
||||||
|
|
||||||
|
x: pos.x
|
||||||
|
y: pos.y
|
||||||
|
}
|
||||||
|
readonly property double gravity: 5000
|
||||||
|
readonly property int constraintRunCount: 40
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
id: shape
|
||||||
|
anchors.fill: parent
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
ShapePath {
|
||||||
|
id: path
|
||||||
|
capStyle: ShapePath.RoundCap
|
||||||
|
strokeColor: "#DAC99B"
|
||||||
|
strokeWidth: 10
|
||||||
|
fillColor: "transparent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: () => {
|
||||||
|
// Create the points
|
||||||
|
const xInc = (end.x - start.x) / segmentCount;
|
||||||
|
const yInc = (end.y - start.y) / segmentCount;
|
||||||
|
let i = 0;
|
||||||
|
while (i < segmentCount) {
|
||||||
|
path.pathElements.push(p.createObject(root, {
|
||||||
|
pos: start.plus(Qt.vector2d(xInc * i, yInc * i)),
|
||||||
|
acc: Qt.vector2d(0, root.gravity)
|
||||||
|
}));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the first and last points
|
||||||
|
path.pathElements[0].acc.y = 0;
|
||||||
|
path.pathElements[segmentCount - 1].acc.y = 0;
|
||||||
|
|
||||||
|
path.startX = path.pathElements[0].x;
|
||||||
|
path.startY = path.pathElements[0].y;
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameAnimation {
|
||||||
|
running: true
|
||||||
|
onTriggered: () => {
|
||||||
|
const dt = frameTime;
|
||||||
|
|
||||||
|
for (const p of path.pathElements.slice(1, root.segmentCount - 2)) {
|
||||||
|
// https://en.wikipedia.org/wiki/Verlet_integration
|
||||||
|
const newPos = p.pos.times(2.0).minus(p.prevPos).plus(p.acc.times(dt * dt));
|
||||||
|
p.prevPos = p.pos;
|
||||||
|
p.pos = newPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < root.segmentCount - 1; i++) {
|
||||||
|
const cur = path.pathElements[i];
|
||||||
|
const next = path.pathElements[i + 1];
|
||||||
|
|
||||||
|
// Keep fixed distance between segments
|
||||||
|
for (let j = 0; j < root.constraintRunCount; j++) {
|
||||||
|
const toNext = next.pos.minus(cur.pos);
|
||||||
|
const distToNext = toNext.length();
|
||||||
|
const error = root.segmentLen - distToNext;
|
||||||
|
const pull = toNext.times(1.0 / distToNext).times(error).times(0.1);
|
||||||
|
if (i != 0) {
|
||||||
|
cur.pos = cur.pos.minus(pull.times(0.5));
|
||||||
|
next.pos = next.pos.plus(pull.times(0.5));
|
||||||
|
} else {
|
||||||
|
next.pos = next.pos.plus(pull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path.pathElements[0].pos = root.start;
|
||||||
|
path.pathElements[root.segmentCount - 1].pos = root.end;
|
||||||
|
|
||||||
|
path.startX = path.pathElements[0].x;
|
||||||
|
path.startY = path.pathElements[0].y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
FloatingWindow {
|
||||||
|
visible: true
|
||||||
|
width: 200
|
||||||
|
height: 100
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
// centers it within parent
|
||||||
|
text: "Hello world!"
|
||||||
|
color: "#0db9d7"
|
||||||
|
font.pixelSize: 18
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import "." as Launcher
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
property bool isOpen: false
|
||||||
|
property var lastUsedTimes: ({})
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "launcher"
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
root.isOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
root.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
root.isOpen = !root.isOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
active: root.isOpen
|
||||||
|
Launcher.Overlay {
|
||||||
|
controller: root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty function to define first reference to singleton
|
||||||
|
function init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import ".."
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var controller
|
||||||
|
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
anchors {
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
top: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
color: "transparent"
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||||
|
WlrLayershell.namespace: "shell:launcher"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: main
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
height: 7 + searchContainer.implicitHeight + list.topMargin + list.bottomMargin + Math.min(list.contentHeight, list.delegateHeight * 10)
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
width: 450
|
||||||
|
color: Globals.isDarkTheme ? "#393536" : "#f3eac7"
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 7
|
||||||
|
anchors.bottomMargin: 0
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: searchContainer
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: search.implicitHeight + 10
|
||||||
|
color: Globals.isDarkTheme ? "#44000000" : "#11000000"
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: search
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: 4
|
||||||
|
color: Globals.isDarkTheme ? "#D6C8A6" : "#403D3B"
|
||||||
|
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
|
||||||
|
focus: true
|
||||||
|
Keys.forwardTo: [list]
|
||||||
|
Keys.onEscapePressed: root.controller.isOpen = false
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
|
if (event.key == Qt.Key_J) {
|
||||||
|
list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1;
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key == Qt.Key_K) {
|
||||||
|
list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (list.currentItem) {
|
||||||
|
list.currentItem.clicked(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
list.currentIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: list
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
cacheBuffer: 0 // works around QTBUG-131106
|
||||||
|
//reuseItems: true
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: DesktopEntries.applications.values.map(object => {
|
||||||
|
const stxt = search.text.toLowerCase();
|
||||||
|
const ntxt = object.name.toLowerCase();
|
||||||
|
|
||||||
|
let si = 0;
|
||||||
|
let ni = 0;
|
||||||
|
|
||||||
|
let matches = [];
|
||||||
|
let startMatch = -1;
|
||||||
|
|
||||||
|
for (let si = 0; si != stxt.length; ++si) {
|
||||||
|
const sc = stxt[si];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Drop any entries with letters that don't exist in order
|
||||||
|
if (ni == ntxt.length)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const nc = ntxt[ni++];
|
||||||
|
|
||||||
|
if (nc == sc) {
|
||||||
|
if (startMatch == -1)
|
||||||
|
startMatch = ni;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
if (startMatch != -1) {
|
||||||
|
matches.push({
|
||||||
|
index: startMatch,
|
||||||
|
length: ni - startMatch
|
||||||
|
});
|
||||||
|
|
||||||
|
startMatch = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startMatch != -1) {
|
||||||
|
matches.push({
|
||||||
|
index: startMatch,
|
||||||
|
length: ni - startMatch + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
object: object,
|
||||||
|
matches: matches
|
||||||
|
};
|
||||||
|
}).filter(entry => entry !== null).sort((a, b) => {
|
||||||
|
if (!search.text) {
|
||||||
|
if (controller.lastUsedTimes[a.object.id])
|
||||||
|
print(controller.lastUsedTimes[a.object.id]);
|
||||||
|
let aDate = controller.lastUsedTimes[a.object.id] ?? new Date(0);
|
||||||
|
let bDate = controller.lastUsedTimes[b.object.id] ?? new Date(0);
|
||||||
|
return bDate - aDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ai = 0;
|
||||||
|
let bi = 0;
|
||||||
|
let s = 0;
|
||||||
|
|
||||||
|
while (ai != a.matches.length && bi != b.matches.length) {
|
||||||
|
const am = a.matches[ai];
|
||||||
|
const bm = b.matches[bi];
|
||||||
|
|
||||||
|
s = bm.length - am.length;
|
||||||
|
if (s != 0)
|
||||||
|
return s;
|
||||||
|
|
||||||
|
s = am.index - bm.index;
|
||||||
|
if (s != 0)
|
||||||
|
return s;
|
||||||
|
|
||||||
|
++ai;
|
||||||
|
++bi;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = a.matches.length - b.matches.length;
|
||||||
|
if (s != 0)
|
||||||
|
return s;
|
||||||
|
|
||||||
|
s = a.object.name.length - b.object.name.length;
|
||||||
|
if (s != 0)
|
||||||
|
return s;
|
||||||
|
|
||||||
|
return a.object.name.localeCompare(b.object.name);
|
||||||
|
}).map(entry => entry.object)
|
||||||
|
|
||||||
|
onValuesChanged: list.currentIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
topMargin: 7
|
||||||
|
bottomMargin: list.count == 0 ? 0 : 7
|
||||||
|
|
||||||
|
add: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displaced: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "y"
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
move: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "y"
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "y"
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight: Rectangle {
|
||||||
|
color: Globals.isDarkTheme ? "#20e0ffff" : "#11000000"
|
||||||
|
}
|
||||||
|
keyNavigationEnabled: true
|
||||||
|
keyNavigationWraps: true
|
||||||
|
highlightMoveVelocity: -1
|
||||||
|
highlightMoveDuration: 100
|
||||||
|
preferredHighlightBegin: list.topMargin
|
||||||
|
preferredHighlightEnd: list.height - list.bottomMargin
|
||||||
|
highlightRangeMode: ListView.ApplyRange
|
||||||
|
snapMode: ListView.SnapToItem
|
||||||
|
|
||||||
|
readonly property real delegateHeight: 44
|
||||||
|
|
||||||
|
delegate: MouseArea {
|
||||||
|
required property DesktopEntry modelData
|
||||||
|
|
||||||
|
implicitHeight: list.delegateHeight
|
||||||
|
implicitWidth: ListView.view.width
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
controller.lastUsedTimes[modelData.id] = new Date();
|
||||||
|
modelData.execute();
|
||||||
|
root.controller.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: delegateLayout
|
||||||
|
anchors {
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
asynchronous: true
|
||||||
|
implicitSize: 30
|
||||||
|
source: Quickshell.iconPath(modelData.icon)
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: modelData.name
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
color: Globals.isDarkTheme ? "#D6C8A6" : "#403D3B"
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MultiEffect {
|
||||||
|
source: main
|
||||||
|
anchors.fill: main
|
||||||
|
shadowBlur: 0
|
||||||
|
shadowEnabled: true
|
||||||
|
shadowColor: "#33000000"
|
||||||
|
shadowVerticalOffset: 16
|
||||||
|
shadowHorizontalOffset: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pam
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
signal pamSuccess
|
||||||
|
signal unlocked
|
||||||
|
signal failed
|
||||||
|
|
||||||
|
// These properties are in the context and not individual lock surfaces
|
||||||
|
// so all surfaces can share the same state.
|
||||||
|
property string currentText: ""
|
||||||
|
property bool unlockInProgress: false
|
||||||
|
property bool showFailure: false
|
||||||
|
|
||||||
|
// Clear the failure text once the user starts typing.
|
||||||
|
onCurrentTextChanged: showFailure = false
|
||||||
|
|
||||||
|
function tryUnlock() {
|
||||||
|
if (currentText === "")
|
||||||
|
return;
|
||||||
|
|
||||||
|
root.unlockInProgress = true;
|
||||||
|
pam.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
PamContext {
|
||||||
|
id: pam
|
||||||
|
|
||||||
|
// Its best to have a custom pam config for quickshell, as the system one
|
||||||
|
// might not be what your interface expects, and break in some way.
|
||||||
|
// This particular example only supports passwords.
|
||||||
|
configDirectory: "pam"
|
||||||
|
config: "password.conf"
|
||||||
|
|
||||||
|
// pam_unix will ask for a response for the password prompt
|
||||||
|
onPamMessage: {
|
||||||
|
if (this.responseRequired) {
|
||||||
|
this.respond(root.currentText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pam_unix won't send any important messages so all we need is the completion status.
|
||||||
|
onCompleted: result => {
|
||||||
|
if (result == PamResult.Success) {
|
||||||
|
root.pamSuccess();
|
||||||
|
} else {
|
||||||
|
root.currentText = "";
|
||||||
|
root.showFailure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.unlockInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import QtQuick
|
||||||
|
import "." as Lock
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
Process {
|
||||||
|
id: screencopyCmd
|
||||||
|
running: false
|
||||||
|
command: ["grim", "-s", "0.5", "-t", "jpeg", "/tmp/lock_screencopy.jpeg"]
|
||||||
|
onExited: lock.locked = true
|
||||||
|
}
|
||||||
|
IpcHandler {
|
||||||
|
target: "lockscreen"
|
||||||
|
|
||||||
|
function lock(): void {
|
||||||
|
screencopyCmd.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WlSessionLock {
|
||||||
|
id: lock
|
||||||
|
locked: false
|
||||||
|
|
||||||
|
WlSessionLockSurface {
|
||||||
|
id: lockSurface
|
||||||
|
color: "transparent"
|
||||||
|
Lock.Surface {
|
||||||
|
anchors.fill: parent
|
||||||
|
context: lockContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Lock.Context {
|
||||||
|
id: lockContext
|
||||||
|
onUnlocked: lock.locked = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty function to define first reference to singleton
|
||||||
|
function init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Controls.Fusion
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell
|
||||||
|
import "." as Lock
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
required property Lock.Context context
|
||||||
|
|
||||||
|
color: "black"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: chest
|
||||||
|
anchors.centerIn: parent
|
||||||
|
source: "../assets/chest.png"
|
||||||
|
sourceClipRect: Qt.rect(32, 0, 32, 32)
|
||||||
|
smooth: false
|
||||||
|
height: width
|
||||||
|
|
||||||
|
// Rectangles
|
||||||
|
RowLayout {
|
||||||
|
visible: passwordBox.enabled
|
||||||
|
anchors.centerIn: parent
|
||||||
|
anchors.verticalCenterOffset: 31
|
||||||
|
spacing: 31
|
||||||
|
Repeater {
|
||||||
|
model: 6
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: r
|
||||||
|
required property var modelData
|
||||||
|
color: root.context.showFailure ? "#D6568F" : "#6BCBF3"
|
||||||
|
implicitWidth: 31
|
||||||
|
implicitHeight: 31
|
||||||
|
MultiEffect {
|
||||||
|
id: glow
|
||||||
|
source: parent
|
||||||
|
anchors.fill: parent
|
||||||
|
brightness: passwordBox.length > r.modelData ? 0.7 : 0
|
||||||
|
blur: passwordBox.length > r.modelData ? 2.0 : 0
|
||||||
|
blurEnabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: screencopy
|
||||||
|
anchors.centerIn: parent
|
||||||
|
source: "/tmp/lock_screencopy.jpeg"
|
||||||
|
height: width * (9 / 16) // Width is animated below
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: chestInFront
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: parent.width / 2
|
||||||
|
source: "../assets/chest.png"
|
||||||
|
sourceClipRect: Qt.rect(32, 16, 32, 16)
|
||||||
|
smooth: false
|
||||||
|
visible: false
|
||||||
|
z: 9999
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter anim
|
||||||
|
// ------------
|
||||||
|
|
||||||
|
// Screencopy anim
|
||||||
|
NumberAnimation {
|
||||||
|
target: screencopy
|
||||||
|
property: "width"
|
||||||
|
from: 1920
|
||||||
|
to: 250
|
||||||
|
duration: 700
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
easing.overshoot: 0.9
|
||||||
|
running: true
|
||||||
|
}
|
||||||
|
SequentialAnimation {
|
||||||
|
id: screencopyAnim
|
||||||
|
running: true
|
||||||
|
NumberAnimation {
|
||||||
|
target: screencopy
|
||||||
|
property: "anchors.verticalCenterOffset"
|
||||||
|
to: -150
|
||||||
|
duration: 500
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
PauseAnimation {
|
||||||
|
duration: 200
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: chestInFront
|
||||||
|
property: "visible"
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: screencopy
|
||||||
|
property: "anchors.verticalCenterOffset"
|
||||||
|
to: 100
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chest anim
|
||||||
|
SequentialAnimation {
|
||||||
|
id: chestAnim
|
||||||
|
running: true
|
||||||
|
NumberAnimation {
|
||||||
|
target: chest
|
||||||
|
property: "width"
|
||||||
|
from: 2000
|
||||||
|
to: 500
|
||||||
|
duration: 500
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
running: true
|
||||||
|
}
|
||||||
|
PauseAnimation {
|
||||||
|
duration: 500
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: chest
|
||||||
|
property: "sourceClipRect.x"
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: chestInFront
|
||||||
|
property: "visible"
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: screencopy
|
||||||
|
property: "visible"
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
onStarted: screencopy.visible = false
|
||||||
|
target: chest
|
||||||
|
property: "anchors.verticalCenterOffset"
|
||||||
|
to: 50
|
||||||
|
duration: 100
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: chest
|
||||||
|
property: "anchors.verticalCenterOffset"
|
||||||
|
to: 0
|
||||||
|
duration: 100
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit anim
|
||||||
|
// ------------
|
||||||
|
|
||||||
|
// Screencopy anim
|
||||||
|
SequentialAnimation {
|
||||||
|
id: screencopyAnimExit
|
||||||
|
running: false
|
||||||
|
PropertyAction {
|
||||||
|
target: chestInFront
|
||||||
|
property: "visible"
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: screencopy
|
||||||
|
property: "anchors.verticalCenterOffset"
|
||||||
|
to: -150
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: chestInFront
|
||||||
|
property: "visible"
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: screencopy
|
||||||
|
property: "anchors.verticalCenterOffset"
|
||||||
|
to: 0
|
||||||
|
duration: 400
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: screencopy
|
||||||
|
property: "width"
|
||||||
|
to: 1920
|
||||||
|
duration: 400
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
running: true
|
||||||
|
}
|
||||||
|
onFinished: () => root.context.unlocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chest anim
|
||||||
|
SequentialAnimation {
|
||||||
|
id: chestAnimExit
|
||||||
|
running: false
|
||||||
|
PropertyAction {
|
||||||
|
target: chest
|
||||||
|
property: "sourceClipRect.x"
|
||||||
|
value: 32
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: screencopy
|
||||||
|
property: "visible"
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: chest
|
||||||
|
property: "anchors.verticalCenterOffset"
|
||||||
|
to: -50
|
||||||
|
duration: 100
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: chest
|
||||||
|
property: "anchors.verticalCenterOffset"
|
||||||
|
to: 0
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: passwordBox
|
||||||
|
|
||||||
|
implicitWidth: 400
|
||||||
|
font.pointSize: 50
|
||||||
|
color: "transparent"
|
||||||
|
cursorVisible: false
|
||||||
|
background: Rectangle {
|
||||||
|
color: "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
focus: true
|
||||||
|
enabled: !screencopyAnim.running && !chestAnim.running && !screencopyAnimExit.running && !chestAnimExit.running && !root.context.unlockInProgress
|
||||||
|
opacity: 0
|
||||||
|
echoMode: TextInput.Password
|
||||||
|
inputMethodHints: Qt.ImhSensitiveData
|
||||||
|
|
||||||
|
onTextChanged: () => {
|
||||||
|
if (text != "") {
|
||||||
|
root.context.showFailure = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: () => {
|
||||||
|
root.context.currentText = text;
|
||||||
|
root.context.tryUnlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.context
|
||||||
|
|
||||||
|
function onUnlockInProgressChanged() {
|
||||||
|
if (root.context.unlockInProgress == false) {
|
||||||
|
passwordBox.text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPamSuccess() {
|
||||||
|
screencopyAnimExit.running = true;
|
||||||
|
chestAnimExit.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button {
|
||||||
|
// text: "Its not working, let me out"
|
||||||
|
// onClicked: root.context.unlocked()
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
auth required pam_unix.so
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// This is how the notification looks
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property Notification notif
|
||||||
|
|
||||||
|
implicitWidth: 240
|
||||||
|
implicitHeight: layout.height
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: trumpetTop
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: width * (sourceSize.height / sourceSize.width)
|
||||||
|
source: "../assets/trumpet-top.png"
|
||||||
|
smooth: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: bannerRect
|
||||||
|
|
||||||
|
color: "#3B253F"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: textColumn.height
|
||||||
|
Layout.leftMargin: (width / trumpetTop.sourceSize.width) * 2 + 3
|
||||||
|
Layout.rightMargin: (width / trumpetTop.sourceSize.width) * 2 + 3
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: textColumn
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.maximumWidth: bannerRect.width
|
||||||
|
text: root.notif.summary + (root.notif.body ? "\n=======" : "")
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
font.pointSize: 18
|
||||||
|
font.bold: true
|
||||||
|
color: "#9292B6"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.maximumWidth: bannerRect.width
|
||||||
|
text: root.notif.body
|
||||||
|
font.family: "BigBlueTermPlusNerdFont"
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
font.pointSize: 14
|
||||||
|
font.bold: false
|
||||||
|
color: "#9292B6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
// This is all the behaviour and animations for the notification
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import "." as Notifs
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal dismissed
|
||||||
|
|
||||||
|
required property var notif
|
||||||
|
property var screen: Qt.size(1920, 1080)
|
||||||
|
|
||||||
|
property var lifetime: 5000
|
||||||
|
|
||||||
|
enum AnimState {
|
||||||
|
Returning,
|
||||||
|
Inert,
|
||||||
|
Flinging,
|
||||||
|
Dismissing
|
||||||
|
}
|
||||||
|
property var state: Notif.Returning
|
||||||
|
property var isDragging: false
|
||||||
|
|
||||||
|
property var initialX: 200
|
||||||
|
property var initialY: 100
|
||||||
|
property var initialR: 45
|
||||||
|
property var targetX: 0
|
||||||
|
property var targetY: 0
|
||||||
|
property var targetR: 0
|
||||||
|
property var velocityX: 0
|
||||||
|
property var velocityY: 0
|
||||||
|
property var velocityR: 0
|
||||||
|
|
||||||
|
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: root.state != Notif.Inert
|
||||||
|
onTriggered: {
|
||||||
|
if (root.state == Notif.Returning) {
|
||||||
|
const deltaX = root.targetX - display.x;
|
||||||
|
const deltaY = root.targetY - display.y;
|
||||||
|
const deltaR = root.targetR - display.rotation;
|
||||||
|
|
||||||
|
root.velocityX = dampingVelocity(root.velocityX, deltaX);
|
||||||
|
root.velocityY = dampingVelocity(root.velocityY, deltaY);
|
||||||
|
root.velocityR = dampingVelocity(root.velocityR, deltaR);
|
||||||
|
|
||||||
|
if (Math.abs(root.velocityX) < 0.1 && Math.abs(root.velocityY) < 0.1) {
|
||||||
|
console.log("inter");
|
||||||
|
root.state = Notif.Inert;
|
||||||
|
root.velocityX = 0;
|
||||||
|
root.velocityY = 0;
|
||||||
|
root.velocityR = 0;
|
||||||
|
display.x = root.targetX;
|
||||||
|
display.y = root.targetY;
|
||||||
|
display.rotation = root.targetR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.isDragging) {
|
||||||
|
if (Math.abs(root.velocityX) > 1200 || Math.abs(root.velocityY) > 1200) {
|
||||||
|
root.state = Notif.Flinging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (root.state == Notif.Flinging) {
|
||||||
|
root.velocityY += 3000 * frameTime;
|
||||||
|
display.rotation = -root.velocityY * frameTime;
|
||||||
|
|
||||||
|
knight.visible = true;
|
||||||
|
knight.x += root.velocityX * frameTime;
|
||||||
|
knight.y += root.velocityY * frameTime;
|
||||||
|
knight.rotation += root.velocityX * 0.2 * frameTime;
|
||||||
|
|
||||||
|
if (display.x > display.width || display.y > root.screen.height) {
|
||||||
|
root.dismissed();
|
||||||
|
}
|
||||||
|
} else if (root.state == Notif.Dismissing) {
|
||||||
|
root.velocityX += frameTime * 20000;
|
||||||
|
|
||||||
|
if (display.x > display.width) {
|
||||||
|
root.dismissed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display.x += root.velocityX * frameTime;
|
||||||
|
display.y += root.velocityY * frameTime;
|
||||||
|
display.rotation += root.velocityR * frameTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: display.width
|
||||||
|
implicitHeight: display.height
|
||||||
|
anchors.fill: display
|
||||||
|
|
||||||
|
Notifs.Display {
|
||||||
|
id: display
|
||||||
|
notif: root.notif
|
||||||
|
x: root.initialX
|
||||||
|
y: root.initialY
|
||||||
|
rotation: root.initialR
|
||||||
|
transformOrigin: Item.Right
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
id: knight
|
||||||
|
source: "../assets/knight.png"
|
||||||
|
x: width
|
||||||
|
width: sourceSize.width * 10
|
||||||
|
height: sourceSize.height * 10
|
||||||
|
smooth: false
|
||||||
|
visible: false
|
||||||
|
Process {
|
||||||
|
id: screamCmd
|
||||||
|
command: ["play", "--no-show-progress", "assets/wilhelm-scream.ogg"]
|
||||||
|
}
|
||||||
|
onVisibleChanged: () => {
|
||||||
|
if (visible) {
|
||||||
|
screamCmd.startDetached();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: display
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
enabled: root.state != Notif.Flinging
|
||||||
|
|
||||||
|
property var prevMouseX: 0
|
||||||
|
property var prevMouseY: 0
|
||||||
|
|
||||||
|
onPressed: e => {
|
||||||
|
if (enabled && e.buttons & Qt.LeftButton) {
|
||||||
|
prevMouseX = e.x;
|
||||||
|
prevMouseY = e.y;
|
||||||
|
root.isDragging = true;
|
||||||
|
root.state = Notif.Returning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: e => {
|
||||||
|
if (!(e.buttons & Qt.LeftButton)) {
|
||||||
|
root.isDragging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPositionChanged: e => {
|
||||||
|
if (enabled && root.isDragging) {
|
||||||
|
root.velocityX = (e.x - prevMouseX) * 200;
|
||||||
|
root.velocityY = (e.y - prevMouseY) * 200;
|
||||||
|
prevMouseX = e.x;
|
||||||
|
prevMouseY = e.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: e => {
|
||||||
|
if (enabled && e.button & Qt.RightButton) {
|
||||||
|
root.state = Notif.Dismissing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: timer
|
||||||
|
interval: root.lifetime
|
||||||
|
repeat: false
|
||||||
|
running: !mouseArea.containsMouse && root.state == Notif.Inert
|
||||||
|
onTriggered: () => {
|
||||||
|
root.state = Notif.Dismissing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: playSoundCmd
|
||||||
|
command: ["play", "--no-show-progress", "assets/trumpet.wav"]
|
||||||
|
}
|
||||||
|
Component.onCompleted: playSoundCmd.startDetached()
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import "."
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var bar
|
||||||
|
property list<Notification> notifs
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "shell:notifications"
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: true
|
||||||
|
top: true
|
||||||
|
bottom: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationServer {
|
||||||
|
actionsSupported: true
|
||||||
|
|
||||||
|
onNotification: notif => {
|
||||||
|
notif.tracked = true;
|
||||||
|
root.notifs = [...root.notifs, notif];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: stack.children.length != 0
|
||||||
|
mask: Region {
|
||||||
|
item: stack
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: stack
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: [...root.notifs]
|
||||||
|
}
|
||||||
|
anchors.right: parent.right
|
||||||
|
y: root.bar.height
|
||||||
|
implicitWidth: 240
|
||||||
|
implicitHeight: children.reduce((h, c) => h + c.height, 0)
|
||||||
|
interactive: false
|
||||||
|
spacing: 20
|
||||||
|
|
||||||
|
displaced: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "y"
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
move: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "y"
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "y"
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Notif {
|
||||||
|
required property Notification modelData
|
||||||
|
notif: modelData
|
||||||
|
|
||||||
|
onDismissed: () => {
|
||||||
|
modelData.dismiss();
|
||||||
|
// Remove from list
|
||||||
|
const index = root.notifs.indexOf(notif);
|
||||||
|
if (index > -1)
|
||||||
|
root.notifs.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import "." as Screenshot
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
property bool isOpen: false
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "screenshot"
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
root.isOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
root.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
root.isOpen = !root.isOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
active: root.isOpen
|
||||||
|
Screenshot.Overlay {
|
||||||
|
controller: root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty function to define first reference to singleton
|
||||||
|
function init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "../components"
|
||||||
|
|
||||||
|
// TODO: make configurable: cursor visibility, screen freeze
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var controller
|
||||||
|
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
anchors {
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
top: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
color: "transparent"
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||||
|
WlrLayershell.namespace: "shell:screenshot"
|
||||||
|
|
||||||
|
visible: screencopy.hasContent
|
||||||
|
ScreencopyView {
|
||||||
|
id: screencopy
|
||||||
|
captureSource: root.screen
|
||||||
|
anchors.fill: parent
|
||||||
|
paintCursor: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: cmd
|
||||||
|
running: false
|
||||||
|
onExited: notifyCmd.running = true
|
||||||
|
}
|
||||||
|
Process {
|
||||||
|
id: notifyCmd
|
||||||
|
command: ["notify-send", "\"Screenshot copied\"", "\"The screenshot was copied to your clipboard\""]
|
||||||
|
running: false
|
||||||
|
onExited: root.controller.isOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem {
|
||||||
|
focus: true
|
||||||
|
Keys.onEscapePressed: root.controller.isOpen = false
|
||||||
|
|
||||||
|
Keys.onReturnPressed: () => {
|
||||||
|
const x = Math.ceil(root.left);
|
||||||
|
const y = Math.ceil(root.top);
|
||||||
|
const w = Math.floor(root.right - root.left);
|
||||||
|
const h = Math.floor(root.bottom - root.top);
|
||||||
|
cmd.command = ["sh", "-c", `grim -g "${x},${y} ${w}x${h}" - | wl-copy`];
|
||||||
|
cmd.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var left: 0
|
||||||
|
property var top: 0
|
||||||
|
property var right: 0
|
||||||
|
property var bottom: 0
|
||||||
|
|
||||||
|
Canvas {
|
||||||
|
id: canvas
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
readonly property var borderExtendX: 56
|
||||||
|
readonly property var borderExtendY: 48
|
||||||
|
readonly property var borderWidthH: 12
|
||||||
|
readonly property var borderWidthV: 18
|
||||||
|
|
||||||
|
onPaint: {
|
||||||
|
var ctx = getContext("2d");
|
||||||
|
ctx.reset();
|
||||||
|
|
||||||
|
// Background
|
||||||
|
ctx.fillStyle = "#44000000";
|
||||||
|
ctx.fillRect(0, 0, root.width, root.height);
|
||||||
|
|
||||||
|
// Border
|
||||||
|
ctx.fillStyle = "#824524";
|
||||||
|
ctx.fillRect(root.left - borderExtendX, root.top - borderWidthH, root.right - root.left + borderExtendX * 2, borderWidthH);
|
||||||
|
ctx.fillRect(root.left - borderExtendX, root.bottom, root.right - root.left + borderExtendX * 2, borderWidthH);
|
||||||
|
ctx.fillRect(root.left - borderWidthV, root.top - borderExtendY, borderWidthV, root.bottom - root.top + borderExtendY * 2);
|
||||||
|
ctx.fillRect(root.right, root.top - borderExtendY, borderWidthV, root.bottom - root.top + borderExtendY * 2);
|
||||||
|
|
||||||
|
// Rect
|
||||||
|
ctx.clearRect(root.left, root.top, root.right - root.left, root.bottom - root.top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rope {
|
||||||
|
id: topLeftRope
|
||||||
|
anchors.fill: parent
|
||||||
|
start: Qt.vector2d(0, 0)
|
||||||
|
end: Qt.vector2d(0, 0)
|
||||||
|
}
|
||||||
|
Rope {
|
||||||
|
id: topRightRope
|
||||||
|
anchors.fill: parent
|
||||||
|
start: Qt.vector2d(0, 0)
|
||||||
|
end: Qt.vector2d(0, 0)
|
||||||
|
}
|
||||||
|
Rope {
|
||||||
|
id: bottomRightRope
|
||||||
|
anchors.fill: parent
|
||||||
|
start: Qt.vector2d(0, 0)
|
||||||
|
end: Qt.vector2d(0, 0)
|
||||||
|
}
|
||||||
|
Rope {
|
||||||
|
id: bottomLeftRope
|
||||||
|
anchors.fill: parent
|
||||||
|
start: Qt.vector2d(0, 0)
|
||||||
|
end: Qt.vector2d(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
RopeTie {
|
||||||
|
x: root.left - width / 2
|
||||||
|
y: root.top - height / 2
|
||||||
|
}
|
||||||
|
RopeTie {
|
||||||
|
x: root.right - width / 2
|
||||||
|
y: root.top - height / 2
|
||||||
|
}
|
||||||
|
RopeTie {
|
||||||
|
x: root.right - width / 2
|
||||||
|
y: root.bottom - height / 2
|
||||||
|
}
|
||||||
|
RopeTie {
|
||||||
|
x: root.left - width / 2
|
||||||
|
y: root.bottom - height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
onPressed: e => {
|
||||||
|
root.left = e.x;
|
||||||
|
root.top = e.y;
|
||||||
|
root.right = e.x;
|
||||||
|
root.bottom = e.y;
|
||||||
|
}
|
||||||
|
onPositionChanged: e => {
|
||||||
|
if (e.x > root.left) {
|
||||||
|
root.right = e.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.y > root.top) {
|
||||||
|
root.bottom = e.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLeftChanged: () => {
|
||||||
|
canvas.requestPaint();
|
||||||
|
topLeftRope.end = Qt.vector2d(left, top);
|
||||||
|
bottomLeftRope.end = Qt.vector2d(left, bottom);
|
||||||
|
}
|
||||||
|
onTopChanged: () => {
|
||||||
|
canvas.requestPaint();
|
||||||
|
topLeftRope.end = Qt.vector2d(left, top);
|
||||||
|
topRightRope.end = Qt.vector2d(right, top);
|
||||||
|
}
|
||||||
|
onRightChanged: () => {
|
||||||
|
canvas.requestPaint();
|
||||||
|
topRightRope.end = Qt.vector2d(right, top);
|
||||||
|
bottomRightRope.end = Qt.vector2d(right, bottom);
|
||||||
|
}
|
||||||
|
onBottomChanged: () => {
|
||||||
|
canvas.requestPaint();
|
||||||
|
bottomRightRope.end = Qt.vector2d(right, bottom);
|
||||||
|
bottomLeftRope.end = Qt.vector2d(left, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
onWidthChanged: () => {
|
||||||
|
left = width / 2;
|
||||||
|
right = width / 2;
|
||||||
|
|
||||||
|
topRightRope.start = Qt.vector2d(width, 0);
|
||||||
|
bottomRightRope.start = Qt.vector2d(width, height);
|
||||||
|
}
|
||||||
|
onHeightChanged: () => {
|
||||||
|
top = height / 2;
|
||||||
|
bottom = height / 2;
|
||||||
|
|
||||||
|
bottomRightRope.start = Qt.vector2d(width, height);
|
||||||
|
bottomLeftRope.start = Qt.vector2d(0, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
component RopeTie: Image {
|
||||||
|
width: 48
|
||||||
|
height: 48
|
||||||
|
source: "../assets/rope-tie.png"
|
||||||
|
smooth: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import "bar" as Bar
|
||||||
|
import "notifs" as Notifs
|
||||||
|
import "launcher" as Launcher
|
||||||
|
import "screenshot" as Screenshot
|
||||||
|
import "lock" as Lock
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
Bg {}
|
||||||
|
|
||||||
|
Bar.Bar {
|
||||||
|
id: bar
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifs.Overlay {
|
||||||
|
bar: bar
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: () => {
|
||||||
|
Launcher.Controller.init();
|
||||||
|
Screenshot.Controller.init();
|
||||||
|
Lock.Controller.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
use hyprland
|
|
||||||
transfer everything
|
|
||||||
list to things to transfere:
|
|
||||||
|
|
||||||
some cursor-themed
|
|
||||||
audacity.x86_64
|
audacity.x86_64
|
||||||
biber.noarch
|
biber.noarch
|
||||||
dolphin.x86_64 may be customized or nautilus cust
|
dolphin.x86_64 may be customized or nautilus cust
|
||||||
gimp.x86_64
|
|
||||||
hyprcursor.x86_64
|
|
||||||
ollama.x86_64
|
ollama.x86_64
|
||||||
plymouth.x86_64
|
plymouth.x86_64
|
||||||
python3.x86_64
|
python3.x86_64
|
||||||
|
|||||||