ported some quickshell stuff from rivendell-hyprdots
@@ -22,6 +22,8 @@
|
||||
|
||||
documentation.man.generateCaches = true; # used for the man script
|
||||
|
||||
qt.enable = true;
|
||||
|
||||
# grub theme
|
||||
boot.loader.grub = {
|
||||
theme = inputs.nixos-grub-themes.packages.${pkgs.system}.nixos; # if you want to use nixos grub theme
|
||||
@@ -207,10 +209,14 @@
|
||||
krita # image edition
|
||||
yt-dlp# some youtube downloader
|
||||
vlc
|
||||
#some nix tools
|
||||
#some nix tools
|
||||
manix
|
||||
deadnix
|
||||
alejandra
|
||||
# libs or apps for quickshell
|
||||
libsForQt5.qt5.qtgraphicaleffects
|
||||
mprisence
|
||||
kdePackages.qt5compat
|
||||
];
|
||||
|
||||
# fonts
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
#../../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/waybar.nix
|
||||
#../../modules/home-manager/oh-my-posh.nix # zsh customizer
|
||||
../../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-zsh.nix # another zsh customizer
|
||||
../../modules/home-manager/hypridle.nix
|
||||
../../modules/home-manager/swaylock.nix
|
||||
|
||||
@@ -228,6 +228,9 @@ in {
|
||||
pseudotile = 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();
|
||||
}
|
||||
}
|
||||