last 7 months of qs changes
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.envrc
|
||||
.direnv/
|
55
modules/user/modules/greetd/default.nix
Normal file
|
@ -0,0 +1,55 @@
|
|||
{ inputs, system, config, lib, pkgs, ... }: let
|
||||
hyprlandPackage = config.home-manager.users.${config.main-user}.wayland.windowManager.hyprland.package;
|
||||
hyprlandConfig = pkgs.writeText "greetd-hyprland-config" ''
|
||||
# for some reason pkill is way faster than dispatching exit, to the point greetd thinks the greeter died.
|
||||
exec-once = quickshell -c greeter >& qslog.txt && pkill Hyprland
|
||||
|
||||
input {
|
||||
kb_layout = us
|
||||
sensitivity = 0
|
||||
follow_mouse = 1
|
||||
# mouse_refocus = false - #6393
|
||||
accel_profile = flat
|
||||
}
|
||||
|
||||
decoration {
|
||||
blur {
|
||||
enabled = no
|
||||
}
|
||||
}
|
||||
|
||||
animations {
|
||||
enabled = no
|
||||
}
|
||||
|
||||
misc {
|
||||
disable_hyprland_logo = true
|
||||
disable_splash_rendering = true
|
||||
background_color = 0x000000
|
||||
key_press_enables_dpms = true
|
||||
mouse_move_enables_dpms = true
|
||||
}
|
||||
|
||||
${config.hyprland-session.extraConfigStatic}
|
||||
'';
|
||||
in {
|
||||
services.greetd = {
|
||||
enable = true;
|
||||
restart = false;
|
||||
settings.default_session.command = "${lib.getExe hyprlandPackage} -c ${hyprlandConfig}";
|
||||
};
|
||||
|
||||
# needed for hyprland cache dir
|
||||
users.users.greeter = {
|
||||
home = "/home/greeter";
|
||||
createHome = true;
|
||||
};
|
||||
|
||||
home-manager.users.greeter = {
|
||||
home.stateVersion = config.system.stateVersion;
|
||||
imports = [
|
||||
../../../theme/home.nix # also fixes cursor
|
||||
../quickshell # set up quickshell manifest and such
|
||||
];
|
||||
};
|
||||
}
|
|
@ -1,16 +1,29 @@
|
|||
{ inputs, pkgs, lib, system, impurity, ... }: let
|
||||
{ config, inputs, pkgs, lib, system, impurity, ... }: let
|
||||
inherit (inputs) quickshell;
|
||||
# hack because the greeter user cant access /home/admin
|
||||
maybeLink = path: if config.home.username == "admin" then impurity.link path else path;
|
||||
in {
|
||||
home.packages = with pkgs; [
|
||||
qt6.qtimageformats # amog
|
||||
qt6.qt5compat # shader fx
|
||||
quickshell.packages.${system}.default
|
||||
pamtester # lockscreen
|
||||
(quickshell.packages.${system}.default.override (prevqs: {
|
||||
debug = true;
|
||||
qt6 = prevqs.qt6.overrideScope (_: prevqt: {
|
||||
qtdeclarative = prevqt.qtdeclarative.overrideAttrs (prev: {
|
||||
cmakeBuildType = "Debug";
|
||||
dontStrip = true;
|
||||
});
|
||||
});
|
||||
|
||||
breakpad = prevqs.breakpad.override rec {
|
||||
stdenv = pkgs.gcc13Stdenv;
|
||||
};
|
||||
}))
|
||||
grim imagemagick # screenshot
|
||||
];
|
||||
|
||||
xdg.configFile."quickshell/manifest.conf".text = lib.generators.toKeyValue {} {
|
||||
shell = "${impurity.link ./shell}";
|
||||
lockscreen = "${impurity.link ./lockscreen}";
|
||||
shell = "${maybeLink ./.}/shell";
|
||||
greeter = "${maybeLink ./.}/shell/greeter.qml";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
QtObject {
|
||||
property int status: AuthContext.Status.FirstLogin
|
||||
signal unlocked();
|
||||
|
||||
enum Status {
|
||||
FirstLogin,
|
||||
Authenticating,
|
||||
LoginFailed
|
||||
}
|
||||
|
||||
property string password
|
||||
|
||||
property var pamtester: Process {
|
||||
property bool failed: true
|
||||
|
||||
command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"]
|
||||
|
||||
onStarted: this.write(`${password}\n`)
|
||||
|
||||
stdout: SplitParser {
|
||||
// fails go to stderr
|
||||
onRead: pamtester.failed = false
|
||||
}
|
||||
|
||||
onExited: {
|
||||
if (failed) {
|
||||
status = AuthContext.Status.LoginFailed;
|
||||
} else {
|
||||
unlocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryLogin(password: string) {
|
||||
this.password = password
|
||||
status = AuthContext.Status.Authenticating;
|
||||
pamtester.running = true;
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
property var time: new Date();
|
||||
property string text;
|
||||
|
||||
Timer {
|
||||
interval: 10000
|
||||
running: true
|
||||
repeat: true
|
||||
|
||||
onTriggered: time = new Date()
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
import Quickshell.Io
|
||||
|
||||
Item {
|
||||
required property AuthContext context
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Text {
|
||||
id: timeText
|
||||
anchors {
|
||||
bottom: entryBox.top
|
||||
bottomMargin: 100
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
font {
|
||||
pointSize: 120
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
family: "Noto Sans"
|
||||
}
|
||||
|
||||
color: "white"
|
||||
|
||||
text: {
|
||||
const hours = LockGlobals.time.getHours().toString().padStart(2, '0');
|
||||
const minutes = LockGlobals.time.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors {
|
||||
top: timeText.bottom
|
||||
topMargin: -20
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
font.pointSize: 40
|
||||
|
||||
color: "#50ffffff"
|
||||
text: "Locked"
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: entryBox
|
||||
anchors.centerIn: parent
|
||||
width: 600
|
||||
font.pointSize: 24
|
||||
|
||||
enabled: context.status != AuthContext.Status.Authenticating
|
||||
focus: true
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
onCursorVisibleChanged: {
|
||||
if (cursorVisible) cursorVisible = false;
|
||||
}
|
||||
cursorVisible: false
|
||||
|
||||
color: "white"
|
||||
|
||||
background: Rectangle {
|
||||
color: "#20ffffff"
|
||||
border.color: "#30ffffff"
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
text: LockGlobals.text
|
||||
onTextChanged: {
|
||||
LockGlobals.text = text
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
if (text != "") context.tryLogin(text)
|
||||
}
|
||||
|
||||
onEnabledChanged: {
|
||||
if (enabled) text = ""
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: status
|
||||
color: "white"
|
||||
font.pointSize: 24
|
||||
|
||||
anchors {
|
||||
horizontalCenter: entryBox.horizontalCenter
|
||||
top: entryBox.bottom
|
||||
topMargin: 40
|
||||
}
|
||||
|
||||
text: {
|
||||
switch (context.status) {
|
||||
case AuthContext.Status.FirstLogin: return ""
|
||||
case AuthContext.Status.Authenticating: return "Authenticating"
|
||||
case AuthContext.Status.LoginFailed: return "Login Failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 20
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: "Turn off Monitors"
|
||||
color: "#aaeeffff"
|
||||
}
|
||||
|
||||
onClicked: dpms.running = true
|
||||
|
||||
background: Rectangle {
|
||||
color: "#20ffffff"
|
||||
border.color: "#30ffffff"
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dpms
|
||||
command: [ "hyprctl", "dispatch", "dpms" ]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
//@ pragma NativeTextRendering
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import ".."
|
||||
|
||||
ShellRoot {
|
||||
AuthContext {
|
||||
id: authContext
|
||||
onUnlocked: lock.locked = false
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: lock
|
||||
locked: true
|
||||
|
||||
onLockedChanged: {
|
||||
if (!locked) Qt.quit();
|
||||
}
|
||||
|
||||
WlSessionLockSurface {
|
||||
id: surface
|
||||
|
||||
BackgroundImage {
|
||||
anchors.fill: parent
|
||||
screen: surface.screen
|
||||
}
|
||||
|
||||
Lockscreen {
|
||||
anchors.fill: parent
|
||||
context: authContext
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import ".."
|
||||
|
||||
ShellRoot {
|
||||
AuthContext {
|
||||
id: authContext
|
||||
onUnlocked: Qt.quit()
|
||||
}
|
||||
|
||||
FloatingWindow {
|
||||
BackgroundImage {
|
||||
anchors.fill: parent
|
||||
screen: Quickshell.screens.filter(s => s.name == "eDP-1")[0]
|
||||
}
|
||||
|
||||
Lockscreen {
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
right: parent.horizontalCenter
|
||||
}
|
||||
|
||||
context: authContext
|
||||
}
|
||||
|
||||
Lockscreen {
|
||||
anchors {
|
||||
left: parent.horizontalCenter
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
context: authContext
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
signal windowOpened(address: string, workspace: string, klass: string, title: string);
|
||||
|
||||
Socket {
|
||||
connected: true
|
||||
path: `/tmp/hypr/${Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")}/.socket2.sock`
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: message => {
|
||||
const [type, body] = message.split(">>");
|
||||
const args = body.split(",");
|
||||
|
||||
switch (type) {
|
||||
case "openwindow":
|
||||
windowOpened(args[0], args[1], args[2], args[3])
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
WlrLayershell {
|
||||
id: root
|
||||
required property var bar;
|
||||
|
||||
property var popup: null;
|
||||
property list<variant> overlays: [];
|
||||
property variant activeOverlay: null;
|
||||
property variant lastActiveOverlay: null;
|
||||
|
||||
onActiveOverlayChanged: {
|
||||
if (lastActiveOverlay != null && lastActiveOverlay != activeOverlay) {
|
||||
lastActiveOverlay.expanded = false;
|
||||
}
|
||||
|
||||
lastActiveOverlay = activeOverlay;
|
||||
}
|
||||
|
||||
readonly property rect barRect: {
|
||||
void [width, height];
|
||||
this.contentItem.mapFromItem(bar, 0, 0, bar.width, bar.height)
|
||||
}
|
||||
|
||||
readonly property real overlayXOffset: barRect.x + barRect.width + 10
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: "transparent"
|
||||
|
||||
namespace: "shell:bar"
|
||||
|
||||
Variants {
|
||||
id: masks
|
||||
model: overlays
|
||||
|
||||
Region {
|
||||
required property var modelData;
|
||||
item: modelData.widget
|
||||
}
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
regions: masks.instances
|
||||
}
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
width: {
|
||||
const extents = overlays
|
||||
.filter(overlay => !overlay.fullyCollapsed)
|
||||
.map(overlay => overlayXOffset + overlay.expandedWidth);
|
||||
|
||||
return Math.max(barRect.x + barRect.width, ...extents);
|
||||
}
|
||||
|
||||
function connectOverlay(overlay: variant) {
|
||||
overlay.widget.parent = this.contentItem
|
||||
overlays.push(overlay);
|
||||
}
|
||||
|
||||
function disconnectOverlay(overlay: variant) {
|
||||
// Splice seems to make it undefined as an intermediary step
|
||||
// which breaks bindings.
|
||||
overlays = overlays.filter(o => o != overlay);
|
||||
}
|
||||
|
||||
function expandedPosition(overlay: variant): rect {
|
||||
const rect = overlay.collapsedLayerRect;
|
||||
|
||||
const idealY = rect.y + (rect.height / 2) - (overlay.expandedHeight / 2)
|
||||
const y = Math.max(barRect.y, Math.min((barRect.y + barRect.height) - overlay.expandedHeight, idealY));
|
||||
|
||||
return Qt.rect(overlayXOffset, y, overlay.expandedWidth, overlay.expandedHeight);
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property var screen;
|
||||
required property var selectionArea;
|
||||
signal selectionComplete(x: real, y: real, width: real, height: real)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onPressed: {
|
||||
selectionArea.startX = mouseX;
|
||||
selectionArea.startY = mouseY;
|
||||
selectionArea.endX = mouseX;
|
||||
selectionArea.endY = mouseY;
|
||||
selectionArea.startSelection(false);
|
||||
}
|
||||
|
||||
onPositionChanged: {
|
||||
selectionArea.endX = mouseX;
|
||||
selectionArea.endY = mouseY;
|
||||
}
|
||||
|
||||
onReleased: selectionArea.endSelection();
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
WlrLayershell {
|
||||
signal selectionComplete(x: real, y: real, width: real, height: real)
|
||||
|
||||
color: "transparent"
|
||||
visible: selectionArea.selecting || selectionArea.initializing
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
layer: WlrLayer.Overlay
|
||||
namespace: "termspawner"
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
right: true
|
||||
top: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
property var selectionArea: area
|
||||
|
||||
Rectangle {
|
||||
id: area
|
||||
property bool selecting: false
|
||||
property bool initializing: false
|
||||
property bool locked: false
|
||||
property real startX: 0
|
||||
property real startY: 0
|
||||
property real endX: 0
|
||||
property real endY: 0
|
||||
|
||||
readonly property bool bigEnough: width > 300 && height > 150
|
||||
|
||||
border.color: bigEnough ? "#ee33ccff" : "#aa595959"
|
||||
border.width: 1
|
||||
radius: 5
|
||||
color: "#66001017"
|
||||
visible: selecting
|
||||
|
||||
x: Math.min(startX, endX) - border.width
|
||||
y: Math.min(startY, endY) - border.width
|
||||
width: Math.max(startX, endX) - x + border.width * 2
|
||||
height: Math.max(startY, endY) - y + border.width * 2
|
||||
|
||||
function startSelection(initialize: bool) {
|
||||
locked = false;
|
||||
if (!initialize) {
|
||||
selecting = true;
|
||||
return;
|
||||
}
|
||||
|
||||
initializing = true
|
||||
if (selecting) {
|
||||
area.startX = mouseArea.mouseX;
|
||||
area.startY = mouseArea.mouseY;
|
||||
area.endX = mouseArea.mouseX;
|
||||
area.endY = mouseArea.mouseY;
|
||||
}
|
||||
}
|
||||
|
||||
function endSelection() {
|
||||
const wasSelecting = selecting;
|
||||
initializing = false;
|
||||
|
||||
if (wasSelecting && bigEnough) {
|
||||
locked = true;
|
||||
selectionComplete(x + 1, y + 1, width - 2, height - 2);
|
||||
} else {
|
||||
selecting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
hoverEnabled: true
|
||||
onPositionChanged: {
|
||||
if (area.initializing) {
|
||||
if (!containsMouse) {
|
||||
area.initializing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
area.startX = mouseX;
|
||||
area.startY = mouseY;
|
||||
area.initializing = false;
|
||||
area.selecting = true;
|
||||
}
|
||||
|
||||
if (!selectionArea.locked) {
|
||||
area.endX = mouseX;
|
||||
area.endY = mouseY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,16 +24,6 @@ Singleton {
|
|||
curve.type: Easing.InQuart
|
||||
}
|
||||
|
||||
property var time: new Date();
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
|
||||
onTriggered: time = new Date()
|
||||
}
|
||||
|
||||
function interpolateColors(x: real, a: color, b: color): color {
|
||||
const xa = 1.0 - x;
|
||||
return Qt.rgba(a.r * xa + b.r * x, a.g * xa + b.g * x, a.b * xa + b.b * x, a.a * xa + b.a * x);
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Singleton {
|
||||
readonly property alias termSelect: termSelectBind.pressed;
|
||||
signal screenshot();
|
||||
|
||||
Shortcut {
|
||||
name: "screenshot"
|
||||
onPressed: screenshot()
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
id: termSelectBind
|
||||
name: "termselect"
|
||||
IpcHandler {
|
||||
target: "screenshot"
|
||||
function takeScreenshot() { screenshot(); }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import Quickshell.Hyprland
|
||||
|
||||
GlobalShortcut {
|
||||
appid: "shell"
|
||||
}
|
112
modules/user/modules/quickshell/shell/SlideView.qml
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
47
modules/user/modules/quickshell/shell/SlideViewItem.qml
Normal file
|
@ -0,0 +1,47 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
@ -5,7 +6,8 @@ import Quickshell
|
|||
import "systray" as SysTray
|
||||
import "audio" as Audio
|
||||
import "mpris" as Mpris
|
||||
import "workspaces" as Workspaces
|
||||
import "power" as Power
|
||||
import "root:notifications" as Notifs
|
||||
|
||||
BarContainment {
|
||||
id: root
|
||||
|
@ -13,6 +15,7 @@ BarContainment {
|
|||
property bool isSoleBar: Quickshell.screens.length == 1;
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
|
@ -21,24 +24,32 @@ BarContainment {
|
|||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Loader {
|
||||
active: isSoleBar
|
||||
Layout.preferredHeight: active ? implicitHeight : 0;
|
||||
Notifs.NotificationWidget {
|
||||
Layout.fillWidth: true
|
||||
|
||||
sourceComponent: Workspaces.Widget {
|
||||
bar: root
|
||||
wsBaseIndex: 1
|
||||
}
|
||||
bar: root
|
||||
}
|
||||
|
||||
Workspaces.Widget {
|
||||
bar: root
|
||||
Layout.fillWidth: true
|
||||
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
|
||||
hideWhenEmpty: isSoleBar
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Loader {
|
||||
active: root.isSoleBar
|
||||
Layout.preferredHeight: active ? implicitHeight : 0;
|
||||
Layout.fillWidth: true
|
||||
|
||||
sourceComponent: Workspaces {
|
||||
bar: root
|
||||
wsBaseIndex: 1
|
||||
}
|
||||
}
|
||||
|
||||
Workspaces {
|
||||
bar: root
|
||||
Layout.fillWidth: true
|
||||
wsBaseIndex: root.screen.name == "eDP-1" ? 11 : 1;
|
||||
hideWhenEmpty: root.isSoleBar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,8 +76,15 @@ BarContainment {
|
|||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ClockWidget {
|
||||
Power.Power {
|
||||
bar: root
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ClockWidget {
|
||||
bar: root
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
34
modules/user/modules/quickshell/shell/bar/BarButton.qml
Normal file
|
@ -0,0 +1,34 @@
|
|||
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 }
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "root:."
|
||||
import "root:lock" as Lock
|
||||
import ".."
|
||||
import "../lock" as Lock
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
@ -20,8 +20,8 @@ PanelWindow {
|
|||
exclusiveZone: width - margins.left
|
||||
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "shell:bar"
|
||||
|
||||
WlrLayershell.namespace: "shell:bar"
|
||||
|
||||
readonly property var tooltip: tooltip;
|
||||
Tooltip {
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import QtQuick
|
||||
|
||||
BarWidgetInner {
|
||||
implicitHeight: 50
|
||||
|
||||
SequentialAnimation on implicitHeight {
|
||||
loops: Animation.Infinite
|
||||
PropertyAnimation { to: 70; duration: 1000 }
|
||||
PropertyAnimation { to: 40; duration: 1000 }
|
||||
}
|
||||
|
||||
property int len: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: `8${'='.repeat(len)}D`
|
||||
font.pointSize: 16
|
||||
color: "white"
|
||||
|
||||
PropertyAnimation on rotation {
|
||||
loops: Animation.Infinite
|
||||
to: 365
|
||||
duration: 1000
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: len += 1;
|
||||
}
|
||||
}
|
19
modules/user/modules/quickshell/shell/bar/ClickableIcon.qml
Normal file
|
@ -0,0 +1,19 @@
|
|||
import QtQuick
|
||||
|
||||
BarButton {
|
||||
id: root
|
||||
required property string image;
|
||||
property alias cache: imageComponent.cache;
|
||||
property alias asynchronous: imageComponent.asynchronous;
|
||||
property bool scaleIcon: !asynchronous
|
||||
|
||||
Image {
|
||||
id: imageComponent
|
||||
anchors.fill: parent
|
||||
|
||||
source: root.image
|
||||
sourceSize.width: scaleIcon ? width : (root.width - baseMargin)
|
||||
sourceSize.height: scaleIcon ? height : (root.height - baseMargin)
|
||||
cache: false
|
||||
}
|
||||
}
|
|
@ -1,41 +1,67 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import ".."
|
||||
|
||||
OverlayWidget {
|
||||
expandedWidth: 600
|
||||
expandedHeight: 600
|
||||
BarWidgetInner {
|
||||
id: root
|
||||
required property var bar;
|
||||
|
||||
BarWidgetInner {
|
||||
implicitHeight: layout.implicitHeight
|
||||
implicitHeight: layout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
spacing: 0
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: tooltip.visible ? SystemClock.Seconds : SystemClock.Minutes;
|
||||
}
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
BarButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
fillWindowWidth: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
spacing: 0
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
const hours = clock.hours.toString().padStart(2, '0')
|
||||
const minutes = clock.minutes.toString().padStart(2, '0')
|
||||
return `${hours}\n${minutes}`
|
||||
}
|
||||
font.pointSize: 18
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property TooltipItem tooltip: TooltipItem {
|
||||
id: tooltip
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
show: button.containsMouse
|
||||
|
||||
Loader {
|
||||
active: tooltip.visible
|
||||
sourceComponent: Label {
|
||||
text: {
|
||||
// SystemClock can send an update slightly (<50ms) before the
|
||||
// time changes. We use its readout so the widget and tooltip match.
|
||||
const hours = clock.hours.toString().padStart(2, '0');
|
||||
const minutes = clock.minutes.toString().padStart(2, '0');
|
||||
const seconds = clock.seconds.toString().padStart(2, '0');
|
||||
|
||||
return `${hours}:${minutes}:${seconds}\n` + new Date().toLocaleString(Qt.locale("en_US"), "dddd, MMMM d, yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: ShellGlobals.time.getHours().toString().padStart(2, '0')
|
||||
font.pointSize: 18
|
||||
color: "#a0ffffff"
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: ShellGlobals.time.getMinutes().toString().padStart(2, '0')
|
||||
font.pointSize: 18
|
||||
color: "#a0ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: expanded = !expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import ".."
|
||||
|
||||
Item {
|
||||
required property var bar;
|
||||
required property real expandedWidth;
|
||||
required property real expandedHeight;
|
||||
required default property Item widget;
|
||||
|
||||
property bool expanded: false;
|
||||
|
||||
onExpandedChanged: {
|
||||
animateTo(expanded ? 1.0 : 0.0)
|
||||
if (expanded) popupSurface.activeOverlay = this
|
||||
}
|
||||
|
||||
readonly property bool fullyCollapsed: animationProgress == 0.0;
|
||||
|
||||
onFullyCollapsedChanged: {
|
||||
if (fullyCollapsed && popupSurface.activeOverlay == this) {
|
||||
popupSurface.activeOverlay = null;
|
||||
}
|
||||
|
||||
/*if (fullyCollapsed) {
|
||||
widget.x = Qt.binding(() => this.x)
|
||||
}*/
|
||||
}
|
||||
|
||||
readonly property rect collapsedLayerRect: {
|
||||
void [barWindow.windowTransform, popupSurface.windowTransform, y, parent.y];
|
||||
return this.mapToItem(popupSurface.contentItem, 0, 0, width, height);
|
||||
}
|
||||
|
||||
onCollapsedLayerRectChanged: console.log(`clr: ${collapsedLayerRect}`)
|
||||
onLayerRectChanged: console.log(`lr: ${layerRect}`)
|
||||
onYChanged: console.log(`y: ${y}`)
|
||||
|
||||
readonly property rect expandedLayerRect: bar.widgetSurface.expandedPosition(this)
|
||||
|
||||
readonly property rect layerRect: {
|
||||
const [p, xCurve, yCurve] = [animationProgress, ShellGlobals.popoutXCurve, ShellGlobals.popoutYCurve];
|
||||
|
||||
return Qt.rect(
|
||||
xCurve.interpolate(p, collapsedLayerRect.x, expandedLayerRect.x),
|
||||
yCurve.interpolate(p, collapsedLayerRect.y, expandedLayerRect.y),
|
||||
xCurve.interpolate(p, collapsedLayerRect.width, expandedLayerRect.width),
|
||||
yCurve.interpolate(p, collapsedLayerRect.height, expandedLayerRect.height),
|
||||
);
|
||||
}
|
||||
|
||||
implicitWidth: widget.implicitWidth
|
||||
implicitHeight: widget.implicitHeight
|
||||
|
||||
Component.onCompleted: {
|
||||
popupSurface.connectOverlay(this);
|
||||
widget.x = Qt.binding(() => layerRect.x);
|
||||
widget.y = Qt.binding(() => layerRect.y);
|
||||
widget.width = Qt.binding(() => layerRect.width);
|
||||
widget.height = Qt.binding(() => layerRect.height);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
popupSurface.disconnectOverlay(this)
|
||||
}
|
||||
|
||||
function animateTo(target: real) {
|
||||
animationProgressInternal = target * 1000
|
||||
}
|
||||
|
||||
property real animationProgress: animationProgressInternal * 0.001
|
||||
property real animationProgressInternal: 0.0 // animations seem to only have int precision
|
||||
|
||||
Behavior on animationProgressInternal {
|
||||
SmoothedAnimation { velocity: 3000 }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onPressed: expanded = false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
border.color: ShellGlobals.colors.widgetOutline
|
||||
border.width: 1
|
||||
radius: 5
|
||||
color: "transparent"
|
||||
opacity: mouseArea.containsMouse ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
SmoothedAnimation { velocity: 4 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
default property Item item;
|
||||
property int expandedWidth;
|
||||
property int expandedHeight;
|
||||
|
||||
implicitHeight: item.implicitHeight
|
||||
implicitWidth: item.implicitWidth
|
||||
|
||||
Component.onCompleted: {
|
||||
item.width = Qt.binding(() => this.width)
|
||||
item.height = Qt.binding(() => this.height)
|
||||
}
|
||||
|
||||
children: [ item ]
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "root:/"
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
@ -11,11 +12,14 @@ Scope {
|
|||
|
||||
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) {
|
||||
|
@ -24,10 +28,12 @@ Scope {
|
|||
}
|
||||
|
||||
if (lastActiveItem != null && lastActiveItem != activeItem) {
|
||||
lastActiveItem.targetVisible = false;
|
||||
if (activeItem != null) lastActiveItem.targetVisible = false;
|
||||
else if (root.hangTime == 0) doLastHide();
|
||||
else hangTimer.start();
|
||||
}
|
||||
|
||||
lastActiveItem = activeItem;
|
||||
if (activeItem != null) lastActiveItem = activeItem;
|
||||
}
|
||||
|
||||
function setItem(item: TooltipItem) {
|
||||
|
@ -46,28 +52,59 @@ Scope {
|
|||
}
|
||||
}
|
||||
|
||||
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: activeItem != null
|
||||
activeAsync: shownItem != null
|
||||
|
||||
PopupWindow {
|
||||
id: popup
|
||||
parentWindow: bar
|
||||
relativeX: bar.tooltipXOffset
|
||||
relativeY: 0
|
||||
height: bar.height
|
||||
width: 1000//Math.max(1, widthAnim.running ? Math.max(tooltipItem.targetWidth, tooltipItem.lastTargetWidth) : tooltipItem.targetWidth)
|
||||
|
||||
anchor {
|
||||
window: bar
|
||||
rect.x: bar.tooltipXOffset
|
||||
rect.y: tooltipItem.highestAnimY
|
||||
adjustment: PopupAdjustment.None
|
||||
}
|
||||
|
||||
HyprlandWindow.opacity: root.scaleMul
|
||||
|
||||
//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: "#20000000"
|
||||
|
||||
mask: Region {
|
||||
item: (activeItem?.hoverable ?? false) ? tooltipItem : null
|
||||
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: activeItem?.isMenu ?? false
|
||||
windows: [ popup, bar ]
|
||||
windows: [ popup, bar, ...(activeItem?.grabWindows ?? []) ]
|
||||
onActiveChanged: {
|
||||
if (!active && activeItem?.isMenu) {
|
||||
activeMenu.close()
|
||||
|
@ -75,30 +112,76 @@ Scope {
|
|||
}
|
||||
}
|
||||
|
||||
/*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.activeItem) {
|
||||
root.activeItem.parent = 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
|
||||
BarWidgetInner {
|
||||
anchors.fill: parent
|
||||
color: ShellGlobals.colors.bar
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var targetWidth: activeItem?.implicitWidth ?? 0;
|
||||
readonly property var targetHeight: activeItem?.implicitHeight ?? 0;
|
||||
onTargetYChanged: updateYBounds();
|
||||
onTargetHeightChanged: updateYBounds();
|
||||
function updateYBounds() {
|
||||
if (targetY - targetHeight / 2 < highestAnimY) {
|
||||
//highestAnimY = targetY - targetHeight / 2
|
||||
}
|
||||
|
||||
property var lastTargetWidthTracker: 0;
|
||||
property var lastTargetWidth: 0;
|
||||
|
||||
onTargetWidthChanged: {
|
||||
lastTargetWidth = lastTargetWidthTracker;
|
||||
lastTargetWidthTracker = targetWidth;
|
||||
if (targetY + targetHeight / 2 > lowestAnimY) {
|
||||
//lowestAnimY = targetY + targetHeight / 2
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real targetY: {
|
||||
if (activeItem == null) return 0;
|
||||
const target = bar.contentItem.mapFromItem(activeItem.owner, 0, activeItem.targetRelativeY).y;
|
||||
return bar.boundedY(target, activeItem.implicitHeight / 2);
|
||||
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
|
||||
|
@ -107,15 +190,24 @@ Scope {
|
|||
property var y1: -1
|
||||
property var y2: -1
|
||||
|
||||
y: y1
|
||||
y: y1 - popup.anchor.rect.y
|
||||
height: y2 - y1
|
||||
|
||||
SmoothedAnimation {
|
||||
target: tooltipItem;
|
||||
property: "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 || !(activeItem?.animateSize ?? true)) {
|
||||
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
|
||||
stop();
|
||||
tooltipItem.y1 = to;
|
||||
} else {
|
||||
|
@ -125,12 +217,11 @@ Scope {
|
|||
}
|
||||
}
|
||||
|
||||
SmoothedAnimation {
|
||||
target: tooltipItem
|
||||
property: "y2"
|
||||
SmoothedAnimation on y2 {
|
||||
id: y2Anim
|
||||
to: tooltipItem.targetY + tooltipItem.targetHeight / 2;
|
||||
onToChanged: {
|
||||
if (tooltipItem.y2 == -1 || !(activeItem?.animateSize ?? true)) {
|
||||
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
|
||||
stop();
|
||||
tooltipItem.y2 = to;
|
||||
} else {
|
||||
|
@ -140,13 +231,11 @@ Scope {
|
|||
}
|
||||
}
|
||||
|
||||
SmoothedAnimation {
|
||||
SmoothedAnimation on w {
|
||||
id: widthAnim
|
||||
target: tooltipItem
|
||||
property: "w"
|
||||
to: tooltipItem.targetWidth;
|
||||
onToChanged: {
|
||||
if (tooltipItem.w == -1) {
|
||||
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
|
||||
stop();
|
||||
tooltipItem.w = to;
|
||||
} else {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/"
|
||||
|
||||
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;
|
||||
|
@ -17,47 +17,35 @@ Item {
|
|||
|
||||
signal close();
|
||||
|
||||
readonly property alias contentItem: contentItem;
|
||||
default property alias data: contentItem.data;
|
||||
|
||||
property Component backgroundComponent: BarWidgetInner {
|
||||
color: ShellGlobals.colors.bar
|
||||
anchors.fill: parent
|
||||
}
|
||||
property Component backgroundComponent: null
|
||||
|
||||
onShowChanged: {
|
||||
if (show) {
|
||||
hangTimer.stop();
|
||||
tooltip.setItem(this);
|
||||
} else if (hangTime == 0) {
|
||||
tooltip.removeItem(this);
|
||||
} else hangTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hangTimer
|
||||
interval: hangTime
|
||||
onTriggered: tooltip.removeItem(root);
|
||||
if (show) tooltip.setItem(this);
|
||||
else tooltip.removeItem(this);
|
||||
}
|
||||
|
||||
property bool targetVisible: false
|
||||
property real targetOpacity: 0
|
||||
opacity: targetOpacity / 1000
|
||||
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
|
||||
|
||||
Behavior on targetOpacity {
|
||||
id: opacityAnimation
|
||||
SmoothedAnimation { velocity: 5000 }
|
||||
SmoothedAnimation { velocity: 5 }
|
||||
}
|
||||
|
||||
function snapOpacity(opacity: real) {
|
||||
opacityAnimation.enabled = false;
|
||||
targetOpacity = opacity * 1000
|
||||
targetOpacity = opacity;
|
||||
opacityAnimation.enabled = true;
|
||||
}
|
||||
|
||||
onTargetVisibleChanged: {
|
||||
if (targetVisible) {
|
||||
visible = true;
|
||||
targetOpacity = 1000;
|
||||
targetOpacity = 1;
|
||||
} else {
|
||||
close()
|
||||
targetOpacity = 0;
|
||||
|
@ -68,20 +56,21 @@ Item {
|
|||
if (!targetVisible && targetOpacity == 0) {
|
||||
visible = false;
|
||||
this.parent = null;
|
||||
if (tooltip) tooltip.onHidden(this);
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
clip: true
|
||||
implicitHeight: contentItem.implicitHeight + 10
|
||||
implicitWidth: contentItem.implicitWidth + 10
|
||||
//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.visible || root.preloadBackground
|
||||
active: root.backgroundComponent && (root.visible || root.preloadBackground)
|
||||
asynchronous: !root.visible && root.preloadBackground
|
||||
sourceComponent: backgroundComponent
|
||||
}
|
||||
|
@ -91,7 +80,7 @@ Item {
|
|||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
|
||||
implicitHeight: childrenRect.height
|
||||
implicitWidth: childrenRect.width
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
pragma ComponentBehavior: Bound;
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
import ".."
|
||||
import "root:."
|
||||
|
||||
MouseArea {
|
||||
FullwidthMouseArea {
|
||||
id: root
|
||||
required property var bar;
|
||||
required property int wsBaseIndex;
|
||||
property int wsCount: 10;
|
||||
property bool hideWhenEmpty: false;
|
||||
|
||||
implicitHeight: column.implicitHeight + 10;
|
||||
|
||||
fillWindowWidth: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
onWheel: event => {
|
||||
|
@ -29,9 +33,6 @@ MouseArea {
|
|||
property int existsCount: 0;
|
||||
visible: !hideWhenEmpty || existsCount > 0;
|
||||
|
||||
property real animPos: 0;
|
||||
Behavior on animPos { SmoothedAnimation { velocity: 100 } }
|
||||
|
||||
// destructor takes care of nulling
|
||||
signal workspaceAdded(workspace: HyprlandWorkspace);
|
||||
|
||||
|
@ -45,20 +46,22 @@ MouseArea {
|
|||
}
|
||||
|
||||
Repeater {
|
||||
model: 10
|
||||
model: root.wsCount
|
||||
|
||||
MouseArea {
|
||||
FullwidthMouseArea {
|
||||
id: wsItem
|
||||
onPressed: Hyprland.dispatch(`workspace ${wsIndex}`);
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 15
|
||||
|
||||
fillWindowWidth: true
|
||||
|
||||
required property int index;
|
||||
property int wsIndex: wsBaseIndex + index;
|
||||
property int wsIndex: root.wsBaseIndex + index;
|
||||
property HyprlandWorkspace workspace: null;
|
||||
property bool exists: workspace != null;
|
||||
property bool active: (monitor?.activeWorkspace ?? false) && monitor.activeWorkspace == workspace;
|
||||
property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace;
|
||||
|
||||
onActiveChanged: {
|
||||
if (active) root.currentIndex = wsIndex;
|
||||
|
@ -78,21 +81,21 @@ MouseArea {
|
|||
}
|
||||
}
|
||||
|
||||
property real animActive: active ? 100 : 0
|
||||
Behavior on animActive { NumberAnimation { duration: 100 } }
|
||||
property real animActive: active ? 1 : 0
|
||||
Behavior on animActive { NumberAnimation { duration: 150 } }
|
||||
|
||||
property real animExists: exists ? 100 : 0
|
||||
property real animExists: exists ? 1 : 0
|
||||
Behavior on animExists { NumberAnimation { duration: 100 } }
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
height: 10
|
||||
width: parent.width
|
||||
scale: 1 + animActive * 0.003
|
||||
scale: 1 + wsItem.animActive * 0.3
|
||||
radius: height / 2
|
||||
border.color: ShellGlobals.colors.widgetOutline
|
||||
border.width: 1
|
||||
color: ShellGlobals.interpolateColors(animExists * 0.01, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
|
||||
color: ShellGlobals.interpolateColors(animExists, ShellGlobals.colors.widget, ShellGlobals.colors.widgetActive);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,14 +12,20 @@ ClickableIcon {
|
|||
|
||||
implicitHeight: width;
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton;
|
||||
showPressed: mixerOpen
|
||||
fillWindowWidth: true
|
||||
showPressed: mixerOpen || (pressedButtons & ~Qt.RightButton)
|
||||
|
||||
onPressed: event => {
|
||||
event.accepted = true;
|
||||
if (event.button === Qt.RightButton) {
|
||||
mixerOpen = !mixerOpen;
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: event => {
|
||||
event.accepted = true;
|
||||
if (event.button === Qt.LeftButton) {
|
||||
event.accepted = true;
|
||||
node.audio.muted = !node.audio.muted;
|
||||
} else if (event.button === Qt.RightButton) {
|
||||
mixerOpen = !mixerOpen;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,7 +77,16 @@ ClickableIcon {
|
|||
sourceComponent: Mixer {
|
||||
width: 550
|
||||
trackedNode: node
|
||||
nodeList: Pipewire.nodes.values.filter(n => n.audio && !n.isStream && n.isSink == node.isSink)
|
||||
nodeImage: root.image
|
||||
|
||||
onSelected: n => {
|
||||
if (node.isSink) {
|
||||
Pipewire.preferredDefaultAudioSink = n;
|
||||
} else {
|
||||
Pipewire.preferredDefaultAudioSource = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import ".."
|
||||
import "../.."
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property PwNode trackedNode;
|
||||
required property string nodeImage;
|
||||
required property list<PwNode> nodeList;
|
||||
|
||||
signal selected(node: PwNode);
|
||||
|
||||
PwNodeLinkTracker {
|
||||
id: linkTracker
|
||||
|
@ -15,10 +21,13 @@ ColumnLayout {
|
|||
|
||||
PwObjectTracker { objects: [ trackedNode, ...linkTracker.linkGroups ] }
|
||||
|
||||
MixerEntry {
|
||||
MixerEntry/*WithSelect*/ {
|
||||
id: nodeEntry
|
||||
node: trackedNode
|
||||
//nodeList: root.nodeList
|
||||
image: nodeImage
|
||||
|
||||
Component.onCompleted: this.selected.connect(root.selected);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
@ -49,7 +58,7 @@ ColumnLayout {
|
|||
// special cases :(
|
||||
if (icon == "firefox") icon = "firefox-devedition";
|
||||
|
||||
return `image://icon/${icon}`
|
||||
return Quickshell.iconPath(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +1,11 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Pipewire
|
||||
import ".."
|
||||
|
||||
RowLayout {
|
||||
MixerEntryBase {
|
||||
id: root
|
||||
required property PwNode node;
|
||||
required property string image;
|
||||
property int state: PwLinkState.Unlinked;
|
||||
|
||||
PwObjectTracker { objects: [ node ] }
|
||||
|
||||
ClickableIcon {
|
||||
image: root.image
|
||||
asynchronous: true
|
||||
implicitHeight: 40
|
||||
implicitWidth: height
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Item {
|
||||
implicitHeight: title.implicitHeight
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
id: title
|
||||
color: "white"
|
||||
anchors.fill: parent
|
||||
elide: Text.ElideRight
|
||||
text: {
|
||||
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
|
||||
const mediaName = node.properties["media.name"];
|
||||
|
||||
return mediaName != undefined ? `${name} - ${mediaName}` : name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VolumeSlider {
|
||||
//Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: 200
|
||||
|
||||
value: node.audio.volume
|
||||
onValueChanged: node.audio.volume = value
|
||||
}
|
||||
headerComponent: Text {
|
||||
color: "white"
|
||||
elide: Text.ElideRight
|
||||
text: root.getNodeName(root.node)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Pipewire
|
||||
import ".."
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property PwNode node;
|
||||
required property string image;
|
||||
required property Item headerComponent;
|
||||
|
||||
property int state: PwLinkState.Unlinked;
|
||||
|
||||
function getNodeName(node: PwNode): string {
|
||||
const name = node.properties["application.name"] ?? (node.description == "" ? node.name : node.description);
|
||||
const mediaName = node.properties["media.name"];
|
||||
|
||||
return mediaName != undefined ? `${name} - ${mediaName}` : name + node.id;
|
||||
}
|
||||
|
||||
PwObjectTracker { objects: [ node ] }
|
||||
|
||||
ClickableIcon {
|
||||
image: root.image
|
||||
asynchronous: true
|
||||
implicitHeight: 40
|
||||
implicitWidth: height
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Item {
|
||||
id: container
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: headerComponent.implicitWidth
|
||||
implicitHeight: headerComponent.implicitHeight
|
||||
|
||||
children: [ headerComponent ]
|
||||
Binding { root.headerComponent.anchors.fill: container }
|
||||
}
|
||||
|
||||
VolumeSlider {
|
||||
Layout.fillWidth: true
|
||||
|
||||
value: node.audio.volume
|
||||
onValueChanged: node.audio.volume = value
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
MixerEntryBase {
|
||||
id: root
|
||||
required property list<PwNode> nodeList;
|
||||
|
||||
signal selected(node: PwNode);
|
||||
|
||||
headerComponent: ComboBox {
|
||||
model: nodeList.map(node => root.getNodeName(node));
|
||||
currentIndex: nodeList.findIndex(node => node == root.node)
|
||||
onActivated: index => root.selected(nodeList[index])
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M806-56 677.67-184.33q-27 18.66-58 32.16-31 13.5-64.34 21.17v-68.67q20-6.33 38.84-13.66 18.83-7.34 35.5-19l-154.34-155V-160l-200-200h-160v-240H262L51.33-810.67 98.67-858l754.66 754L806-56Zm-26.67-232-48-48q19-33 28.17-69.62 9.17-36.61 9.17-75.38 0-100.22-58.34-179.11Q652-739 555.33-762.33V-831q124 28 202 125.5t78 224.5q0 51.67-14.16 100.67-14.17 49-41.84 92.33Zm-134-134-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5t-7.5 28.5Zm-170-170-104-104 104-104v208Zm-66.66 270v-131.33l-80-80H182v106.66h122L408.67-322Zm-40-171.33Z"/></svg>
|
Before (image error) Size: 650 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e8eaed"><path d="M560-131v-68.67q94.67-27.33 154-105 59.33-77.66 59.33-176.33 0-98.67-59-176.67-59-78-154.33-104.66V-831q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm426.67 45.33v-332Q599-628 629.5-582T660-480q0 55-30.83 100.83-30.84 45.84-82.5 64.5ZM413.33-634l-104 100.67H186.67v106.66h122.66l104 101.34V-634Zm-96 154Z"/></svg>
|
Before (image error) Size: 474 B |
|
@ -82,7 +82,7 @@ BarWidgetInner {
|
|||
x: blurRadius
|
||||
width: blur.width - blurRadius * 2
|
||||
height: blur.height
|
||||
clip: true
|
||||
|
||||
GaussianBlur {
|
||||
source: blurSource
|
||||
x: -parent.x
|
||||
|
@ -139,23 +139,27 @@ BarWidgetInner {
|
|||
readonly property Rectangle overlay: overlayItem;
|
||||
Rectangle {
|
||||
id: overlayItem
|
||||
visible: false
|
||||
visible: true
|
||||
anchors.fill: parent
|
||||
border.color: ShellGlobals.colors.widgetOutlineSeparate
|
||||
border.width: 0//1
|
||||
radius: 0//root.radius
|
||||
radius: root.radius
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.radius
|
||||
color: "transparent"
|
||||
border.color: ShellGlobals.colors.widgetOutlineSeparate;
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
|
||||
// slightly offset on the corners :/
|
||||
layer.enabled: true
|
||||
layer.effect: ShaderEffect {
|
||||
fragmentShader: "radial_clip.frag.qsb"
|
||||
// +1 seems to match Rectangle
|
||||
property real radius: root.radius + 1
|
||||
property size size: Qt.size(root.width, root.height)
|
||||
property real borderWidth: 1//.5
|
||||
property color borderColor: ShellGlobals.colors.widgetOutlineSeparate//"#ffff0000"
|
||||
property color tint: overlayItem.color
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: root.width
|
||||
height: root.height
|
||||
radius: root.radius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQml.Models
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Hyprland
|
||||
import "../.."
|
||||
|
||||
Singleton {
|
||||
|
@ -15,51 +17,58 @@ Singleton {
|
|||
property bool __reverse: false;
|
||||
|
||||
property var activeTrack;
|
||||
Component.onCompleted: {
|
||||
for (const player of Mpris.players.values) {
|
||||
if (player.playbackState == MprisPlaybackState.Playing) {
|
||||
if (root.trackedPlayer == null) {
|
||||
root.trackedPlayer = player;
|
||||
|
||||
Instantiator {
|
||||
model: Mpris.players;
|
||||
|
||||
Connections {
|
||||
required property MprisPlayer modelData;
|
||||
target: modelData;
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.trackedPlayer == null || modelData.isPlaying) {
|
||||
root.trackedPlayer = modelData;
|
||||
}
|
||||
}
|
||||
|
||||
player.playbackStateChanged.connect(() => {
|
||||
if (root.trackedPlayer !== player) root.trackedPlayer = player;
|
||||
});
|
||||
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 onTrackChanged() {
|
||||
function onPostTrackChanged() {
|
||||
root.updateTrack();
|
||||
}
|
||||
}
|
||||
|
||||
// Change the tracked player when one changes playback state or is created in a playing state.
|
||||
Connections {
|
||||
target: Mpris.players;
|
||||
function onTrackArtUrlChanged() {
|
||||
console.log("arturl:", activePlayer.trackArtUrl)
|
||||
//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;
|
||||
|
||||
function onObjectInsertedPost(player: MprisPlayer) {
|
||||
if (player.playbackState === MprisPlaybackState.Playing) {
|
||||
if (root.trackedPlayer !== player) root.trackedPlayer = player;
|
||||
}
|
||||
|
||||
player.playbackStateChanged.connect(() => {
|
||||
if (root.trackedPlayer !== player) root.trackedPlayer = player;
|
||||
});
|
||||
}
|
||||
|
||||
function onObjectRemovedPre() {
|
||||
console.log(`trackedPlayer: ${root.trackedPlayer}`)
|
||||
if (root.trackedPlayer == null) {
|
||||
for (const player of Mpris.players.values) {
|
||||
if (player.playbackState === MprisPlaybackState.Playing) {
|
||||
root.trackedPlayer = player;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,27 +76,23 @@ Singleton {
|
|||
onActivePlayerChanged: this.updateTrack();
|
||||
|
||||
function updateTrack() {
|
||||
const metadata = this.activePlayer?.metadata ?? {};
|
||||
|
||||
//console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
|
||||
this.activeTrack = {
|
||||
artUrl: metadata["mpris:artUrl"] ?? "",
|
||||
title: metadata["xesam:title"] ?? "",
|
||||
artist: metadata["xesam:artist"] ?? "",
|
||||
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.playbackState == MprisPlaybackState.Playing;
|
||||
property bool canPlay: this.activePlayer?.canPlay ?? false;
|
||||
function play() {
|
||||
if (this.canPlay) this.activePlayer.playbackState = MprisPlaybackState.Playing;
|
||||
}
|
||||
|
||||
property bool canPause: this.activePlayer?.canPause ?? false;
|
||||
function pause() {
|
||||
if (this.canPause) this.activePlayer.playbackState = MprisPlaybackState.Paused;
|
||||
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;
|
||||
|
@ -125,7 +130,7 @@ Singleton {
|
|||
}
|
||||
|
||||
function setActivePlayer(player: MprisPlayer) {
|
||||
const targetPlayer = player ?? MprisPlayer.players[0];
|
||||
const targetPlayer = player ?? Mpris.players[0];
|
||||
console.log(`setactive: ${targetPlayer} from ${activePlayer}`)
|
||||
|
||||
if (targetPlayer && this.activePlayer) {
|
||||
|
@ -138,31 +143,17 @@ Singleton {
|
|||
this.trackedPlayer = targetPlayer;
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
name: "music-pauseall";
|
||||
onPressed: {
|
||||
for (let i = 0; i < Mpris.players.length; i++) {
|
||||
const player = Mpris.players[i];
|
||||
if (player.canPause) player.playbackState = MprisPlaybackState.Paused;
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
|
||||
function pauseAll(): void {
|
||||
for (const player of Mpris.players.values) {
|
||||
if (player.canPause) player.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
name: "music-playpause";
|
||||
onPressed: {
|
||||
if (root.isPlaying) root.pause();
|
||||
else root.play();
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
name: "music-previous";
|
||||
onPressed: root.previous();
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
name: "music-next";
|
||||
onPressed: root.next();
|
||||
function playPause(): void { root.togglePlaying(); }
|
||||
function previous(): void { root.previous(); }
|
||||
function next(): void { root.next(); }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Scope {
|
||||
required property MprisPlayer player;
|
||||
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
import QtQuick
|
||||
import
|
|
@ -1,3 +1,5 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
@ -7,9 +9,10 @@ import Quickshell.Services.Mpris
|
|||
import ".."
|
||||
import "../.."
|
||||
|
||||
MouseArea {
|
||||
FullwidthMouseArea {
|
||||
id: root
|
||||
hoverEnabled: true
|
||||
fillWindowWidth: true
|
||||
|
||||
required property var bar;
|
||||
implicitHeight: column.implicitHeight + 10
|
||||
|
@ -27,12 +30,12 @@ MouseArea {
|
|||
property alias widgetOpen: persist.widgetOpen;
|
||||
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: widgetOpen = !widgetOpen
|
||||
onPressed: widgetOpen = !widgetOpen
|
||||
|
||||
onWheel: event => {
|
||||
event.accepted = true;
|
||||
if (MprisController.canChangeVolume) {
|
||||
this.activePlayer.volume = Math.max(0, Math.min(1, this.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
|
||||
root.activePlayer.volume = Math.max(0, Math.min(1, root.activePlayer.volume + (event.angleDelta.y / 120) * 0.05));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +45,7 @@ MouseArea {
|
|||
id: widget
|
||||
anchors.fill: parent
|
||||
|
||||
property real scaleMul: root.pressed || widgetOpen ? 100 : 1
|
||||
property real scaleMul: widgetOpen ? 100 : 1
|
||||
Behavior on scaleMul { SmoothedAnimation { velocity: 600 } }
|
||||
scale: scaleCurve.interpolate(scaleMul / 100, 1, (width - 6) / width)
|
||||
|
||||
|
@ -56,8 +59,10 @@ MouseArea {
|
|||
BackgroundArt {
|
||||
id: bkg
|
||||
anchors.fill: parent
|
||||
overlay.color: "#30000000"
|
||||
|
||||
function updateArt(reverse: bool) {
|
||||
console.log("update art", MprisController.activeTrack.artUrl)
|
||||
this.setArt(MprisController.activeTrack.artUrl, reverse, false)
|
||||
}
|
||||
|
||||
|
@ -86,6 +91,7 @@ MouseArea {
|
|||
implicitHeight: width
|
||||
scaleIcon: false
|
||||
baseMargin: 3
|
||||
hoverEnabled: false
|
||||
enabled: MprisController.canGoPrevious;
|
||||
onClicked: MprisController.previous();
|
||||
}
|
||||
|
@ -95,11 +101,9 @@ MouseArea {
|
|||
image: `root:icons/${MprisController.isPlaying ? "pause" : "play"}.svg`;
|
||||
implicitHeight: width
|
||||
scaleIcon: false
|
||||
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay;
|
||||
onClicked: {
|
||||
if (MprisController.isPlaying) MprisController.pause();
|
||||
else MprisController.play();
|
||||
}
|
||||
hoverEnabled: false
|
||||
enabled: MprisController.canTogglePlaying;
|
||||
onClicked: MprisController.togglePlaying();
|
||||
}
|
||||
|
||||
ClickableIcon {
|
||||
|
@ -108,51 +112,162 @@ MouseArea {
|
|||
implicitHeight: width
|
||||
scaleIcon: false
|
||||
baseMargin: 3
|
||||
hoverEnabled: false
|
||||
enabled: MprisController.canGoNext;
|
||||
onClicked: MprisController.next();
|
||||
}
|
||||
}
|
||||
|
||||
property var tooltip: TooltipItem {
|
||||
property Scope positionInfo: Scope {
|
||||
id: positionInfo
|
||||
|
||||
property var player: root.activePlayer;
|
||||
property int position: Math.floor(player.position);
|
||||
property int length: Math.floor(player.length);
|
||||
|
||||
FrameAnimation {
|
||||
id: posTracker;
|
||||
running: positionInfo.player.isPlaying && (tooltip.visible || rightclickMenu.visible);
|
||||
onTriggered: positionInfo.player.positionChanged();
|
||||
}
|
||||
|
||||
function timeStr(time: int): string {
|
||||
const seconds = time % 60;
|
||||
const minutes = Math.floor(time / 60);
|
||||
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
|
||||
property TooltipItem tooltip: TooltipItem {
|
||||
id: tooltip
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
|
||||
show: root.containsMouse && (activePlayer?.metadata["mpris:trackid"] ?? false)
|
||||
show: root.containsMouse
|
||||
|
||||
//implicitHeight: root.height - 10
|
||||
//implicitWidth: childrenRect.width
|
||||
/*ColumnLayout {
|
||||
ColumnLayout {
|
||||
visible: MprisController.activePlayer != null
|
||||
|
||||
Label { text: MprisController.activeTrack?.title ?? "" }
|
||||
|
||||
Label {
|
||||
text: {
|
||||
const artist = MprisController.activeTrack?.artist ?? "";
|
||||
const album = MprisController.activeTrack?.album ?? "";
|
||||
|
||||
return artist + (album ? ` - ${album}` : "");
|
||||
}
|
||||
}
|
||||
|
||||
Label { text: MprisController.activePlayer?.identity ?? "" }
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: MprisController.activePlayer == null
|
||||
text: "No media playing"
|
||||
}
|
||||
|
||||
Rectangle { implicitHeight: 10; color: "white"; Layout.fillWidth: true }
|
||||
}*/
|
||||
|
||||
contentItem.anchors.margins: 0
|
||||
|
||||
Item {
|
||||
implicitWidth: 200
|
||||
implicitHeight: 100
|
||||
}
|
||||
id: ttcontent
|
||||
width: parent.width
|
||||
height: Math.max(parent.height, implicitHeight)
|
||||
implicitWidth: cl.implicitWidth + 10
|
||||
implicitHeight: cl.implicitHeight + 10 + (MprisController.activePlayer ? 8 : 0)
|
||||
|
||||
/*Loader {
|
||||
active: tooltip.visible
|
||||
ColumnLayout {
|
||||
id: cl
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
margins: 5
|
||||
}
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
height: root.height - 10
|
||||
RowLayout {
|
||||
Image {
|
||||
Layout.fillHeight: true
|
||||
source: mainPlayer.metadata["mpris:artUrl"] ?? ""
|
||||
//visible: MprisController.activePlayer != null
|
||||
|
||||
FontMetrics { id: fontmetrics }
|
||||
|
||||
component FullheightLabel: Item {
|
||||
implicitHeight: fontmetrics.height
|
||||
implicitWidth: label.implicitWidth
|
||||
|
||||
property alias text: label.text
|
||||
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: height
|
||||
sourceSize.height: height
|
||||
}
|
||||
Label {
|
||||
text: mainPlayer.identity
|
||||
id: label
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Slider {
|
||||
Layout.fillWidth: true
|
||||
FullheightLabel {
|
||||
visible: MprisController.activePlayer != null
|
||||
text: MprisController.activeTrack?.title ?? ""
|
||||
}
|
||||
|
||||
FullheightLabel {
|
||||
visible: MprisController.activePlayer != null
|
||||
text: MprisController.activeTrack?.artist ?? ""
|
||||
/*text: {
|
||||
const artist = MprisController.activeTrack?.artist ?? "";
|
||||
const album = MprisController.activeTrack?.album ?? "";
|
||||
|
||||
return artist + (album ? ` - ${album}` : "");
|
||||
}*/
|
||||
}
|
||||
|
||||
Label {
|
||||
text: {
|
||||
if (!MprisController.activePlayer) return "No media playing";
|
||||
|
||||
return MprisController.activePlayer?.identity + " - "
|
||||
+ positionInfo.timeStr(positionInfo.position) + " / "
|
||||
+ positionInfo.timeStr(positionInfo.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
Rectangle {
|
||||
id: ttprect
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
color: "#30ceffff"
|
||||
implicitHeight: 8
|
||||
visible: MprisController.activePlayer != null
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
color: "#80ceffff"
|
||||
width: parent.width * (root.activePlayer.position / root.activePlayer.length)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: ttcontent.width
|
||||
height: ttcontent.height
|
||||
bottomLeftRadius: 5
|
||||
bottomRightRadius: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var rightclickMenu: TooltipItem {
|
||||
|
@ -182,6 +297,7 @@ MouseArea {
|
|||
target: MprisController
|
||||
|
||||
function onTrackChanged(reverse: bool) {
|
||||
console.log(`track changed: rev: ${reverse}`)
|
||||
popupBkg.setArt(MprisController.activeTrack.artUrl, reverse, false);
|
||||
}
|
||||
}
|
||||
|
@ -191,71 +307,41 @@ MouseArea {
|
|||
}
|
||||
}
|
||||
|
||||
contentItem {
|
||||
implicitWidth: 500
|
||||
implicitHeight: 650
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: rightclickMenu.visible
|
||||
width: 500
|
||||
height: 650
|
||||
active: rightclickMenu.visible
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
property var player: activePlayer;
|
||||
anchors.fill: parent;
|
||||
|
||||
property int position: 0;
|
||||
property int length: 0;
|
||||
|
||||
FrameAnimation {
|
||||
id: posTracker;
|
||||
running: player.playbackState == MprisPlaybackState.Playing && widgetOpen;
|
||||
onTriggered: player.positionChanged();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: player
|
||||
|
||||
function onPositionChanged() {
|
||||
const newPosition = Math.floor(player.position);
|
||||
if (newPosition != position) position = newPosition;
|
||||
}
|
||||
|
||||
function onLengthChanged() {
|
||||
const newLength = Math.floor(player.length);
|
||||
if (newLength != length) length = newLength;
|
||||
}
|
||||
}
|
||||
property var player: root.activePlayer;
|
||||
|
||||
Connections {
|
||||
target: MprisController
|
||||
|
||||
function onTrackChanged(reverse: bool) {
|
||||
trackStack.updateTrack(reverse, false);
|
||||
length = Math.floor(player.length);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
position = Math.floor(player.position);
|
||||
length = Math.floor(player.length);
|
||||
}
|
||||
|
||||
function timeStr(time: int): string {
|
||||
const seconds = time % 60;
|
||||
const minutes = Math.floor(time / 60);
|
||||
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
Item {
|
||||
id: playerSelectorContainment
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: playerSelector.implicitHeight + 20
|
||||
implicitWidth: playerSelector.implicitWidth
|
||||
|
||||
ScrollView {
|
||||
RowLayout { //ScrollView {
|
||||
id: playerSelector
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(implicitWidth, playerSelectorContainment.width)
|
||||
|
||||
RowLayout {
|
||||
//RowLayout {
|
||||
Repeater {
|
||||
model: Mpris.players
|
||||
|
||||
|
@ -281,8 +367,7 @@ MouseArea {
|
|||
source: {
|
||||
const entry = DesktopEntries.byId(modelData.desktopEntry);
|
||||
console.log(`ent ${entry} id ${modelData.desktopEntry}`)
|
||||
if (!entry) return "image://icon/";
|
||||
return `image://icon/${entry.icon}`;
|
||||
return Quickshell.iconPath(entry?.icon);
|
||||
}
|
||||
//asynchronous: true
|
||||
|
||||
|
@ -292,18 +377,18 @@ MouseArea {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 10
|
||||
Layout.bottomMargin: 20
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: activePlayer.identity
|
||||
text: root.activePlayer.identity
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -311,7 +396,13 @@ MouseArea {
|
|||
id: trackStack
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 400
|
||||
clip: animating || (lastFlicked?.contentX ?? 0) != 0
|
||||
|
||||
// inverse of default tooltip margin - 1px for border
|
||||
Layout.leftMargin: -4
|
||||
Layout.rightMargin: -4
|
||||
|
||||
property Flickable lastFlicked;
|
||||
property bool reverse: false;
|
||||
|
||||
Component.onCompleted: updateTrack(false, true);
|
||||
|
@ -333,14 +424,16 @@ MouseArea {
|
|||
// but may take longer if the image is huge.
|
||||
readonly property bool svReady: img.status === Image.Ready;
|
||||
contentWidth: width + 1
|
||||
onDragStarted: trackStack.lastFlicked = this
|
||||
onDragEnded: {
|
||||
return;
|
||||
//return;
|
||||
console.log(`dragend ${contentX}`)
|
||||
if (Math.abs(contentX) > 75) {
|
||||
if (contentX < 0) MprisController.previous();
|
||||
else if (contentX > 0) MprisController.next();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: trackContent
|
||||
width: flickable.width
|
||||
|
@ -348,7 +441,7 @@ MouseArea {
|
|||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 300//img.implicitHeight
|
||||
implicitHeight: 302//img.implicitHeight
|
||||
implicitWidth: img.implicitWidth
|
||||
|
||||
Image {
|
||||
|
@ -362,27 +455,50 @@ MouseArea {
|
|||
|
||||
sourceSize.height: 300
|
||||
sourceSize.width: 300
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
cached: true
|
||||
maskSource: Rectangle {
|
||||
width: img.width
|
||||
height: img.height
|
||||
radius: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
component CenteredText: Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 20
|
||||
|
||||
property alias text: label.text
|
||||
property alias font: label.font
|
||||
|
||||
Label {
|
||||
id: label
|
||||
visible: text != ""
|
||||
anchors.centerIn: parent
|
||||
text: track.title
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(parent.width - 20, implicitWidth)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
CenteredText {
|
||||
Layout.topMargin: 20
|
||||
text: track.title
|
||||
font.pointSize: albumLabel.font.pointSize + 1
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: track.artist
|
||||
}
|
||||
CenteredText {
|
||||
id: albumLabel
|
||||
Layout.topMargin: 18
|
||||
text: track.album
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
CenteredText {
|
||||
Layout.topMargin: 25
|
||||
text: track.artist
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
@ -462,11 +578,8 @@ MouseArea {
|
|||
implicitWidth: 80
|
||||
implicitHeight: width
|
||||
scaleIcon: false
|
||||
enabled: MprisController.isPlaying ? MprisController.canPause : MprisController.canPlay
|
||||
onClicked: {
|
||||
if (MprisController.isPlaying) MprisController.pause();
|
||||
else MprisController.play();
|
||||
}
|
||||
enabled: MprisController.canTogglePlaying;
|
||||
onClicked: MprisController.togglePlaying();
|
||||
}
|
||||
|
||||
ClickableIcon {
|
||||
|
@ -491,33 +604,70 @@ MouseArea {
|
|||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.margins: 5
|
||||
|
||||
Label {
|
||||
Layout.preferredWidth: lengthLabel.implicitWidth
|
||||
text: timeStr(position)
|
||||
text: positionInfo.timeStr(positionInfo.position)
|
||||
}
|
||||
|
||||
MediaSlider {
|
||||
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 = player.position / player.length;
|
||||
const bpos = boundAnimStart;
|
||||
return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor));
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: boundAnim
|
||||
target: slider
|
||||
property: "boundAnimFactor"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 600
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: player
|
||||
|
||||
function onPositionChanged() {
|
||||
if (false && player.position == 0 && slider.lastPosition != 0 && !boundAnim.running) {
|
||||
slider.boundAnimStart = slider.lastPosition / slider.lastLength;
|
||||
boundAnim.start();
|
||||
}
|
||||
|
||||
slider.lastPosition = player.position;
|
||||
slider.lastLength = player.length;
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
property var bindSlider: true;
|
||||
enabled: player.canSeek
|
||||
from: 0
|
||||
to: player.length
|
||||
to: 1
|
||||
|
||||
onPressedChanged: {
|
||||
if (!pressed) player.position = value;
|
||||
if (!pressed) player.position = value * player.length;
|
||||
bindSlider = !pressed;
|
||||
}
|
||||
|
||||
Binding {
|
||||
when: slider.bindSlider
|
||||
slider.value: player.position
|
||||
slider.value: slider.boundPosition
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: lengthLabel
|
||||
text: timeStr(length)
|
||||
text: positionInfo.timeStr(positionInfo.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
import "root:."
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property UPowerDevice device;
|
||||
property real scale: 1;
|
||||
|
||||
readonly property bool isCharging: root.device.state == UPowerDeviceState.Charging;
|
||||
readonly property bool isPluggedIn: isCharging || root.device.state == UPowerDeviceState.PendingCharge;
|
||||
readonly property bool isLow: root.device.percentage <= 0.20;
|
||||
|
||||
width: 35 * root.scale
|
||||
height: 35 * root.scale
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 4 * root.scale
|
||||
}
|
||||
|
||||
width: 13 * root.scale
|
||||
height: 23 * root.device.percentage * root.scale
|
||||
radius: 2 * root.scale
|
||||
|
||||
color: root.isPluggedIn ? "#359040"
|
||||
: ShellGlobals.interpolateColors(Math.min(1.0, Math.min(0.5, root.device.percentage) * 2), "red", "white")
|
||||
}
|
||||
|
||||
Image {
|
||||
id: img
|
||||
anchors.fill: parent;
|
||||
|
||||
source: root.isCharging ? "root:icons/battery-charging.svg"
|
||||
: root.isPluggedIn ? "root:icons/battery-plus.svg"
|
||||
: root.isLow ? "root:icons/battery-warning.svg"
|
||||
: "root:icons/battery-empty.svg"
|
||||
|
||||
sourceSize.width: parent.width
|
||||
sourceSize.height: parent.height
|
||||
visible: true
|
||||
}
|
||||
}
|
224
modules/user/modules/quickshell/shell/bar/power/Power.qml
Normal file
|
@ -0,0 +1,224 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import Quickshell.Widgets
|
||||
import ".."
|
||||
import "root:."
|
||||
import "root:components"
|
||||
import "power"
|
||||
|
||||
BarWidgetInner {
|
||||
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 UPowerDevice batteryDevice: UPower.devices.values
|
||||
.find(device => device.isLaptopBattery);
|
||||
|
||||
function statusStr() {
|
||||
return root.isPluggedIn ? `Plugged in, ${root.isCharging ? "Charging" : "Not Charging"}`
|
||||
: "Discharging";
|
||||
}
|
||||
|
||||
property bool showMenu: false;
|
||||
|
||||
implicitHeight: width
|
||||
color: isLow ? "#45ff6060" : ShellGlobals.colors.widget
|
||||
|
||||
BarButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
baseMargin: 5
|
||||
fillWindowWidth: true
|
||||
acceptedButtons: Qt.RightButton
|
||||
directScale: true
|
||||
showPressed: root.showMenu
|
||||
|
||||
onPressed: {
|
||||
root.showMenu = !root.showMenu
|
||||
}
|
||||
|
||||
BatteryIcon {
|
||||
device: UPower.displayDevice
|
||||
}
|
||||
}
|
||||
|
||||
property TooltipItem tooltip: TooltipItem {
|
||||
id: tooltip
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
show: button.containsMouse
|
||||
|
||||
Loader {
|
||||
active: tooltip.visible
|
||||
|
||||
sourceComponent: Label {
|
||||
text: {
|
||||
const status = root.statusStr();
|
||||
|
||||
const percentage = Math.round(root.percentage * 100);
|
||||
|
||||
let str = `${percentage}% - ${status}`;
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property TooltipItem rightclickMenu: TooltipItem {
|
||||
id: rightclickMenu
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
|
||||
isMenu: true
|
||||
show: root.showMenu
|
||||
onClose: root.showMenu = false
|
||||
|
||||
Loader {
|
||||
active: rightclickMenu.visible
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: 10
|
||||
|
||||
FontMetrics { id: fm }
|
||||
|
||||
component SmallLabel: Label {
|
||||
font.pointSize: fm.font.pointSize * 0.8
|
||||
color: "#d0eeffff"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
IconImage {
|
||||
source: "root:icons/gauge.svg"
|
||||
implicitSize: 32
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
Label { text: "Power Profile" }
|
||||
|
||||
OptionSlider {
|
||||
values: ["Power Save", "Balanced", "Performance"]
|
||||
index: PowerProfiles.profile
|
||||
onIndexChanged: PowerProfiles.profile = this.index;
|
||||
implicitWidth: 350
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
source: "root:icons/battery-empty.svg"
|
||||
implicitSize: 32
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Label { text: "Battery" }
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: `${root.statusStr()} -`
|
||||
color: "#d0eeffff"
|
||||
}
|
||||
Label { text: `${Math.round(root.percentage * 100)}%` }
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
Layout.fillWidth: true
|
||||
value: UPower.displayDevice.percentage
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: remainingTimeLbl.text !== ""
|
||||
|
||||
SmallLabel { text: "Time remaining" }
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
SmallLabel {
|
||||
id: remainingTimeLbl
|
||||
text: {
|
||||
const device = UPower.displayDevice;
|
||||
const time = device.timeToEmpty || device.timeToFull;
|
||||
|
||||
if (time === 0) return "";
|
||||
const minutes = Math.floor(time / 60).toString().padStart(2, '0');
|
||||
return `${minutes} minutes`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: root.batteryDevice.healthSupported
|
||||
SmallLabel { text: "Health" }
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
SmallLabel {
|
||||
text: `${Math.floor((root.batteryDevice?.healthPercentage ?? 0))}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
// external devices
|
||||
values: UPower.devices.values.filter(device => !device.powerSupply)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
required property UPowerDevice modelData;
|
||||
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
source: {
|
||||
switch (modelData.type) {
|
||||
case UPowerDeviceType.Headset: return "root:icons/headset.svg";
|
||||
}
|
||||
return Quickshell.iconPath(modelData.iconName)
|
||||
}
|
||||
implicitSize: 32
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Label { text: modelData.model }
|
||||
Item { Layout.fillWidth: true }
|
||||
Label { text: `${Math.round(modelData.percentage * 100)}%` }
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
Layout.fillWidth: true
|
||||
value: modelData.percentage
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: modelData.healthSupported
|
||||
SmallLabel { text: "Health" }
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
SmallLabel {
|
||||
text: `${Math.floor(modelData.healthPercentage)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ Item {
|
|||
property bool expanded: false;
|
||||
|
||||
readonly property bool open: progress != 0;
|
||||
readonly property bool animating: internalProgress != -1 && internalProgress != 101;
|
||||
readonly property bool animating: internalProgress != (expanded ? 101 : -1);
|
||||
|
||||
implicitHeight: 16
|
||||
implicitWidth: 16
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.DBusMenu
|
||||
import "../.."
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
required property var entry;
|
||||
required property QsMenuEntry entry;
|
||||
property alias expanded: childrenRevealer.expanded;
|
||||
property bool animating: childrenRevealer.animating || childrenList.animating;
|
||||
property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false);
|
||||
// appears it won't actually create the handler when only used from MenuItemList.
|
||||
onExpandedChanged: {}
|
||||
onAnimatingChanged: {}
|
||||
|
@ -22,7 +23,7 @@ MouseArea {
|
|||
onClicked: {
|
||||
if (entry.hasChildren) childrenRevealer.expanded = !childrenRevealer.expanded
|
||||
else {
|
||||
entry.click();
|
||||
entry.triggered();
|
||||
if (entry.toggleType == ToggleButtonType.None) close();
|
||||
}
|
||||
}
|
||||
|
@ -34,19 +35,21 @@ MouseArea {
|
|||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
id: innerRow
|
||||
|
||||
Item {
|
||||
implicitWidth: 22
|
||||
implicitHeight: 22
|
||||
|
||||
MenuCheckBox {
|
||||
anchors.centerIn: parent
|
||||
visible: entry.toggleType == ToggleButtonType.CheckBox
|
||||
visible: entry.buttonType == QsMenuButtonType.CheckBox
|
||||
checkState: entry.checkState
|
||||
}
|
||||
|
||||
MenuRadioButton {
|
||||
anchors.centerIn: parent
|
||||
visible: entry.toggleType == ToggleButtonType.RadioButton
|
||||
visible: entry.buttonType == QsMenuButtonType.RadioButton
|
||||
checkState: entry.checkState
|
||||
}
|
||||
|
||||
|
@ -59,7 +62,7 @@ MouseArea {
|
|||
}
|
||||
|
||||
Text {
|
||||
text: entry.cleanLabel
|
||||
text: entry.text
|
||||
color: entry.enabled ? "white" : "#bbbbbb"
|
||||
}
|
||||
|
||||
|
@ -68,25 +71,32 @@ MouseArea {
|
|||
implicitWidth: 22
|
||||
implicitHeight: 22
|
||||
|
||||
Image {
|
||||
IconImage {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: entry.icon != ""
|
||||
source: entry.icon
|
||||
sourceSize.height: parent.height
|
||||
sourceSize.width: parent.height
|
||||
visible: source != ""
|
||||
implicitSize: parent.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Loader {
|
||||
id: childMenuLoader
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: childrenList.implicitHeight * childrenRevealer.progress
|
||||
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
|
||||
|
||||
MenuItemList {
|
||||
sourceComponent: MenuView {
|
||||
id: childrenList
|
||||
items: entry.children
|
||||
menu: entry
|
||||
onClose: root.close()
|
||||
|
||||
anchors {
|
||||
|
|
|
@ -6,17 +6,19 @@ import "../.."
|
|||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
required property var items;
|
||||
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: items
|
||||
model: menuView.children;
|
||||
|
||||
Loader {
|
||||
required property var modelData;
|
|
@ -1,3 +1,5 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
@ -5,102 +7,102 @@ import Quickshell
|
|||
import Quickshell.Services.SystemTray
|
||||
import ".."
|
||||
|
||||
OverlayWidget {
|
||||
BarWidgetInner {
|
||||
id: root
|
||||
expandedWidth: 600
|
||||
expandedHeight: 800
|
||||
required property var bar;
|
||||
implicitHeight: column.implicitHeight + 10
|
||||
|
||||
BarWidgetInner {
|
||||
implicitHeight: column.implicitHeight + 10
|
||||
ColumnLayout {
|
||||
id: column
|
||||
implicitHeight: childrenRect.height
|
||||
spacing: 5
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
implicitHeight: childrenRect.height
|
||||
spacing: 5
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: 5
|
||||
}
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: 5
|
||||
}
|
||||
Repeater {
|
||||
model: SystemTray.items;
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items;
|
||||
Item {
|
||||
id: item
|
||||
required property SystemTrayItem modelData;
|
||||
|
||||
Item {
|
||||
required property var modelData;
|
||||
readonly property alias menu: menuWatcher.menu;
|
||||
property bool targetMenuOpen: false;
|
||||
|
||||
SystemTrayMenuWatcher {
|
||||
id: menuWatcher;
|
||||
trayItem: modelData;
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: width
|
||||
|
||||
ClickableIcon {
|
||||
id: mouseArea
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
width: height
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
|
||||
image: item.modelData.icon
|
||||
showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton)
|
||||
fillWindowWidth: true
|
||||
extraVerticalMargin: column.spacing / 2
|
||||
|
||||
onClicked: event => {
|
||||
event.accepted = true;
|
||||
|
||||
if (event.button == Qt.LeftButton) {
|
||||
item.modelData.activate();
|
||||
} else if (event.button == Qt.MiddleButton) {
|
||||
item.modelData.secondaryActivate();
|
||||
}
|
||||
}
|
||||
|
||||
property bool targetMenuOpen: false;
|
||||
onTargetMenuOpenChanged: menu.showChildren = targetMenuOpen
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: width
|
||||
|
||||
ClickableIcon {
|
||||
id: mouseArea
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
onPressed: event => {
|
||||
if (event.button == Qt.RightButton && item.modelData.hasMenu) {
|
||||
item.targetMenuOpen = !item.targetMenuOpen;
|
||||
}
|
||||
width: height
|
||||
}
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onWheel: event => {
|
||||
event.accepted = true;
|
||||
const points = event.angleDelta.y / 120
|
||||
item.modelData.scroll(points, false);
|
||||
}
|
||||
|
||||
image: modelData.icon
|
||||
showPressed: targetMenuOpen
|
||||
property var tooltip: TooltipItem {
|
||||
tooltip: root.bar.tooltip
|
||||
owner: mouseArea
|
||||
|
||||
onClicked: event => {
|
||||
event.accepted = true;
|
||||
show: mouseArea.containsMouse
|
||||
|
||||
if (event.button == Qt.LeftButton) {
|
||||
modelData.activate();
|
||||
} else if (event.button == Qt.MiddleButton) {
|
||||
modelData.secondaryActivate();
|
||||
} else if (event.button == Qt.RightButton && menu != null) {
|
||||
targetMenuOpen = !targetMenuOpen;
|
||||
}
|
||||
Text {
|
||||
id: tooltipText
|
||||
text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: event => {
|
||||
event.accepted = true;
|
||||
const points = event.angleDelta.y / 120
|
||||
modelData.scroll(points, false);
|
||||
}
|
||||
property var rightclickMenu: TooltipItem {
|
||||
id: rightclickMenu
|
||||
tooltip: root.bar.tooltip
|
||||
owner: mouseArea
|
||||
|
||||
property var tooltip: TooltipItem {
|
||||
tooltip: bar.tooltip
|
||||
owner: mouseArea
|
||||
isMenu: true
|
||||
show: item.targetMenuOpen
|
||||
animateSize: !(menuContentLoader?.item?.animating ?? false)
|
||||
|
||||
show: mouseArea.containsMouse
|
||||
onClose: item.targetMenuOpen = false;
|
||||
|
||||
Text {
|
||||
id: tooltipText
|
||||
text: modelData.tooltipTitle != "" ? modelData.tooltipTitle : modelData.id
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: menuContentLoader
|
||||
active: item.targetMenuOpen || rightclickMenu.visible || mouseArea.containsMouse
|
||||
|
||||
property var rightclickMenu: TooltipItem {
|
||||
tooltip: bar.tooltip
|
||||
owner: mouseArea
|
||||
|
||||
isMenu: true
|
||||
show: targetMenuOpen && menu.showChildren
|
||||
animateSize: !rightclickItems.animating
|
||||
|
||||
onClose: targetMenuOpen = false;
|
||||
|
||||
MenuItemList {
|
||||
id: rightclickItems
|
||||
items: menu == null ? [] : menu.children
|
||||
onClose: targetMenuOpen = false;
|
||||
sourceComponent: MenuView {
|
||||
menu: item.modelData.menu
|
||||
onClose: item.targetMenuOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
required property MouseArea target;
|
||||
|
||||
property real velocityX: 0;
|
||||
property real velocityY: 0;
|
||||
|
||||
signal flickStarted();
|
||||
signal flickCompleted();
|
||||
|
||||
property real dragStartX: 0;
|
||||
property real dragStartY: 0;
|
||||
property real dragDeltaX: 0;
|
||||
property real dragDeltaY: 0;
|
||||
property real dragEndX: 0;
|
||||
property real dragEndY: 0;
|
||||
|
||||
property var sampleIdx: -1
|
||||
property var tSamples: []
|
||||
property var xSamples: []
|
||||
property var ySamples: []
|
||||
|
||||
ElapsedTimer { id: velocityTimer }
|
||||
|
||||
function resetSamples() {
|
||||
velocityTimer.restart();
|
||||
sampleIdx = -1;
|
||||
tSamples = [];
|
||||
xSamples = [];
|
||||
ySamples = [];
|
||||
}
|
||||
|
||||
function sample() {
|
||||
const deltaT = velocityTimer.elapsed();
|
||||
|
||||
sampleIdx++;
|
||||
if (sampleIdx > 5) {
|
||||
sampleIdx = 0;
|
||||
}
|
||||
|
||||
tSamples[sampleIdx] = deltaT;
|
||||
xSamples[sampleIdx] = dragDeltaX;
|
||||
ySamples[sampleIdx] = dragDeltaY;
|
||||
}
|
||||
|
||||
function updateVelocity() {
|
||||
let firstIdx = sampleIdx + 1;
|
||||
if (firstIdx > tSamples.length - 1) firstIdx = 0;
|
||||
|
||||
const deltaT = tSamples[sampleIdx] - tSamples[firstIdx];
|
||||
const deltaX = xSamples[sampleIdx] - xSamples[firstIdx];
|
||||
const deltaY = ySamples[sampleIdx] - ySamples[firstIdx];
|
||||
|
||||
root.velocityX = deltaX / deltaT;
|
||||
root.velocityY = deltaY / deltaT;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.target;
|
||||
|
||||
function onPressed(event: MouseEvent) {
|
||||
root.resetSamples();
|
||||
root.dragDeltaX = 0;
|
||||
root.dragDeltaY = 0;
|
||||
root.dragStartX = event.x;
|
||||
root.dragStartY = event.y;
|
||||
root.flickStarted();
|
||||
}
|
||||
|
||||
function onReleased(event: MouseEvent) {
|
||||
root.dragDeltaX = event.x - root.dragStartX;
|
||||
root.dragDeltaY = event.y - root.dragStartY;
|
||||
root.dragEndX = event.x;
|
||||
root.dragEndY = event.y;
|
||||
root.sample();
|
||||
root.updateVelocity();
|
||||
root.flickCompleted();
|
||||
}
|
||||
|
||||
function onPositionChanged(event: MouseEvent) {
|
||||
root.dragDeltaX = event.x - root.dragStartX;
|
||||
root.dragDeltaY = event.y - root.dragStartY;
|
||||
root.sample();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
pragma ComponentBehavior: Bound;
|
||||
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property list<string> values;
|
||||
property int index: 0;
|
||||
|
||||
implicitWidth: 300
|
||||
implicitHeight: 40
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
property real halfHandle: handle.width / 2;
|
||||
property real activeWidth: groove.width - handle.width;
|
||||
property real valueOffset: mouseArea.halfHandle + (root.index / (root.values.length - 1)) * mouseArea.activeWidth;
|
||||
|
||||
Repeater {
|
||||
model: root.values
|
||||
|
||||
Item {
|
||||
id: delegate
|
||||
required property int index;
|
||||
required property string modelData;
|
||||
|
||||
anchors.top: groove.bottom
|
||||
anchors.topMargin: 2
|
||||
x: mouseArea.halfHandle + (delegate.index / (root.values.length - 1)) * mouseArea.activeWidth
|
||||
|
||||
Rectangle {
|
||||
id: mark
|
||||
color: "#60eeffff"
|
||||
width: 1
|
||||
height: groove.height
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.top: mark.bottom
|
||||
|
||||
x: delegate.index === 0 ? -4
|
||||
: delegate.index === root.values.length - 1 ? -this.width + 4
|
||||
: -(this.width / 2);
|
||||
|
||||
text: delegate.modelData
|
||||
color: "#a0eeffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: grooveFill
|
||||
|
||||
anchors {
|
||||
left: groove.left
|
||||
top: groove.top
|
||||
bottom: groove.bottom
|
||||
}
|
||||
|
||||
radius: 5
|
||||
color: "#80ceffff"
|
||||
width: mouseArea.valueOffset
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: groove
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
y: 5
|
||||
implicitHeight: 7
|
||||
color: "transparent"
|
||||
border.color: "#20eeffff"
|
||||
border.width: 1
|
||||
radius: 5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: handle
|
||||
anchors.verticalCenter: groove.verticalCenter
|
||||
height: 15
|
||||
width: height
|
||||
radius: height * 0.5
|
||||
x: mouseArea.valueOffset - width * 0.5
|
||||
}
|
||||
}
|
||||
|
||||
Binding {
|
||||
when: mouseArea.pressed
|
||||
root.index: Math.max(0, Math.min(root.values.length - 1, Math.round((mouseArea.mouseX / root.width) * (root.values.length - 1))));
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real from: 0.0
|
||||
property real to: 1.0
|
||||
property real value: 0.0
|
||||
|
||||
implicitHeight: 7
|
||||
implicitWidth: 200
|
||||
|
||||
Rectangle {
|
||||
id: grooveFill
|
||||
|
||||
anchors {
|
||||
left: groove.left
|
||||
top: groove.top
|
||||
bottom: groove.bottom
|
||||
}
|
||||
|
||||
radius: 5
|
||||
color: "#80ceffff"
|
||||
width: root.width * ((root.value - root.from) / (root.to - root.from))
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: groove
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
height: 7
|
||||
color: "transparent"
|
||||
border.color: "#20eeffff"
|
||||
border.width: 1
|
||||
radius: 5
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
onChildrenChanged: recalc();
|
||||
|
||||
Instantiator {
|
||||
model: root.children
|
||||
|
||||
Connections {
|
||||
required property Item modelData;
|
||||
target: modelData;
|
||||
|
||||
function onImplicitHeightChanged() {
|
||||
root.recalc();
|
||||
}
|
||||
|
||||
function onImplicitWidthChanged() {
|
||||
root.recalc();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recalc() {
|
||||
let y = 0
|
||||
let w = 0
|
||||
for (const child of this.children) {
|
||||
child.y = y;
|
||||
y += child.implicitHeight
|
||||
if (child.implicitWidth > w) w = child.implicitWidth;
|
||||
}
|
||||
|
||||
this.implicitHeight = y;
|
||||
this.implicitWidth = w;
|
||||
}
|
||||
}
|
38
modules/user/modules/quickshell/shell/greeter.qml
Normal file
|
@ -0,0 +1,38 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Greetd
|
||||
import ".."
|
||||
import "lock"
|
||||
|
||||
ShellRoot {
|
||||
GreeterContext {
|
||||
id: context
|
||||
|
||||
onLaunch: {
|
||||
lock.locked = false;
|
||||
Greetd.launch(["hyprland"]);
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: lock
|
||||
locked: true
|
||||
|
||||
WlSessionLockSurface {
|
||||
id: lockSurface
|
||||
|
||||
BackgroundImage {
|
||||
id: backgroundImage
|
||||
anchors.fill: parent
|
||||
screen: lockSurface.screen
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
LockContent {
|
||||
anchors.fill: parent
|
||||
state: context.state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M150.81,131.79a8,8,0,0,1,.35,7.79l-16,32a8,8,0,0,1-14.32-7.16L131.06,144H112a8,8,0,0,1-7.16-11.58l16-32a8,8,0,1,1,14.32,7.16L124.94,128H144A8,8,0,0,1,150.81,131.79ZM96,16h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>
|
After (image error) Size: 499 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M88,8a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,8ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>
|
After (image error) Size: 348 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M88,8a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,8ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Zm-28,76H136V112a8,8,0,0,0-16,0v20H100a8,8,0,0,0,0,16h20v20a8,8,0,0,0,16,0V148h20a8,8,0,0,0,0-16Z"></path></svg>
|
After (image error) Size: 443 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M120,136V96a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,24a12,12,0,1,0,12,12A12,12,0,0,0,128,160ZM96,16h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16ZM200,56V224a24,24,0,0,1-24,24H80a24,24,0,0,1-24-24V56A24,24,0,0,1,80,32h96A24,24,0,0,1,200,56Zm-16,0a8,8,0,0,0-8-8H80a8,8,0,0,0-8,8V224a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8Z"></path></svg>
|
After (image error) Size: 424 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216Z"></path></svg>
|
After (image error) Size: 348 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M53.92,34.62A8,8,0,1,0,42.08,45.38L58.82,63.8A79.59,79.59,0,0,0,48,104c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.8a40,40,0,0,0,78.4,0h15.44l19.44,21.38a8,8,0,1,0,11.84-10.76ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a63.65,63.65,0,0,1,6.26-27.62L168.09,184Zm166-4.73a8.13,8.13,0,0,1-2.93.55,8,8,0,0,1-7.44-5.08C196.35,156.19,192,129.75,192,104A64,64,0,0,0,96.43,48.31a8,8,0,0,1-7.9-13.91A80,80,0,0,1,208,104c0,35.35,8.05,58.59,10.52,64.88A8,8,0,0,1,214,179.25Z"></path></svg>
|
After (image error) Size: 641 B |
1
modules/user/modules/quickshell/shell/icons/bell.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a64,64,0,1,1,128,0c0,36.05,8.28,66.73,16,80Z"></path></svg>
|
After (image error) Size: 424 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M256,128a15.76,15.76,0,0,1-7.33,13.34L160.48,197.5A15.91,15.91,0,0,1,136,184.16v-37.3L56.48,197.5A15.91,15.91,0,0,1,32,184.16V71.84A15.91,15.91,0,0,1,56.48,58.5L136,109.14V71.84A15.91,15.91,0,0,1,160.48,58.5l88.19,56.16A15.76,15.76,0,0,1,256,128Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M256,128a15.76,15.76,0,0,1-7.33,13.34L160.48,197.5A15.91,15.91,0,0,1,136,184.16v-37.3L56.48,197.5A15.91,15.91,0,0,1,32,184.16V71.84A15.91,15.91,0,0,1,56.48,58.5L136,109.14V71.84A15.91,15.91,0,0,1,160.48,58.5l88.19,56.16A15.76,15.76,0,0,1,256,128Z"></path></svg>
|
||||
|
|
Before (image error) Size: 445 B After (image error) Size: 445 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M72,128a134.63,134.63,0,0,1-14.16,60.47,8,8,0,1,1-14.32-7.12A118.8,118.8,0,0,0,56,128,71.73,71.73,0,0,1,83,71.8,8,8,0,1,1,93,84.29,55.76,55.76,0,0,0,72,128Zm56-8a8,8,0,0,0-8,8,184.12,184.12,0,0,1-23,89.1,8,8,0,0,0,14,7.76A200.19,200.19,0,0,0,136,128,8,8,0,0,0,128,120Zm0-32a40,40,0,0,0-40,40,8,8,0,0,0,16,0,24,24,0,0,1,48,0,214.09,214.09,0,0,1-20.51,92A8,8,0,1,0,146,226.83,230,230,0,0,0,168,128,40,40,0,0,0,128,88Zm0-64A104.11,104.11,0,0,0,24,128a87.76,87.76,0,0,1-5,29.33,8,8,0,0,0,15.09,5.33A103.9,103.9,0,0,0,40,128a88,88,0,0,1,176,0,282.24,282.24,0,0,1-5.29,54.45,8,8,0,0,0,6.3,9.4,8.22,8.22,0,0,0,1.55.15,8,8,0,0,0,7.84-6.45A298.37,298.37,0,0,0,232,128,104.12,104.12,0,0,0,128,24ZM94.4,152.17A8,8,0,0,0,85,158.42a151,151,0,0,1-17.21,45.44,8,8,0,0,0,13.86,8,166.67,166.67,0,0,0,19-50.25A8,8,0,0,0,94.4,152.17ZM128,56a72.85,72.85,0,0,0-9,.56,8,8,0,0,0,2,15.87A56.08,56.08,0,0,1,184,128a252.12,252.12,0,0,1-1.92,31A8,8,0,0,0,189,168a8.39,8.39,0,0,0,1,.06,8,8,0,0,0,7.92-7,266.48,266.48,0,0,0,2-33A72.08,72.08,0,0,0,128,56Zm57.93,128.25a8,8,0,0,0-9.75,5.75c-1.46,5.69-3.15,11.4-5,17a8,8,0,0,0,5,10.13,7.88,7.88,0,0,0,2.55.42,8,8,0,0,0,7.58-5.46c2-5.92,3.79-12,5.35-18.05A8,8,0,0,0,185.94,184.26Z"></path></svg>
|
After (image error) Size: 1.3 KiB |
1
modules/user/modules/quickshell/shell/icons/gauge.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M207.06,72.67A111.24,111.24,0,0,0,128,40h-.4C66.07,40.21,16,91,16,153.13V176a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V152A111.25,111.25,0,0,0,207.06,72.67ZM224,176H119.71l54.76-75.3a8,8,0,0,0-12.94-9.42L99.92,176H32V153.13c0-3.08.15-6.12.43-9.13H56a8,8,0,0,0,0-16H35.27c10.32-38.86,44-68.24,84.73-71.66V80a8,8,0,0,0,16,0V56.33A96.14,96.14,0,0,1,221,128H200a8,8,0,0,0,0,16h23.67c.21,2.65.33,5.31.33,8Z"></path></svg>
|
After (image error) Size: 523 B |
1
modules/user/modules/quickshell/shell/icons/headset.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M201.89,54.66A103.43,103.43,0,0,0,128.79,24H128A104,104,0,0,0,24,128v56a24,24,0,0,0,24,24H64a24,24,0,0,0,24-24V144a24,24,0,0,0-24-24H40.36A88.12,88.12,0,0,1,190.54,65.93,87.39,87.39,0,0,1,215.65,120H192a24,24,0,0,0-24,24v40a24,24,0,0,0,24,24h24a24,24,0,0,1-24,24H136a8,8,0,0,0,0,16h56a40,40,0,0,0,40-40V128A103.41,103.41,0,0,0,201.89,54.66ZM64,136a8,8,0,0,1,8,8v40a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V136Zm128,56a8,8,0,0,1-8-8V144a8,8,0,0,1,8-8h24v56Z"></path></svg>
|
After (image error) Size: 570 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z"></path></svg>
|
After (image error) Size: 252 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Z"></path></svg>
|
||||
|
|
Before (image error) Size: 353 B After (image error) Size: 354 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"></path></svg>
|
||||
|
|
Before (image error) Size: 386 B After (image error) Size: 386 B |
|
@ -2,7 +2,7 @@
|
|||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
fill="#ffffff"
|
||||
fill="#f0f0f0"
|
||||
viewBox="0 0 256 256"
|
||||
data-darkreader-inline-fill=""
|
||||
version="1.1"
|
||||
|
|
Before (image error) Size: 580 B After (image error) Size: 580 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z"></path></svg>
|
||||
|
|
Before (image error) Size: 521 B After (image error) Size: 521 B |
|
@ -2,7 +2,7 @@
|
|||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
fill="#ffffff"
|
||||
fill="#f0f0f0"
|
||||
viewBox="0 0 256 256"
|
||||
data-darkreader-inline-fill=""
|
||||
version="1.1"
|
||||
|
|
Before (image error) Size: 957 B After (image error) Size: 957 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Zm-88,40a8,8,0,0,0,8-8V104a8,8,0,0,0-11.58-7.16l-16,8a8,8,0,1,0,7.16,14.31l4.42-2.21V152A8,8,0,0,0,136,160Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M24,128A72.08,72.08,0,0,1,96,56h96V40a8,8,0,0,1,13.66-5.66l24,24a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,192,88V72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H64V168a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,64,216V200h96a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Zm-88,40a8,8,0,0,0,8-8V104a8,8,0,0,0-11.58-7.16l-16,8a8,8,0,1,0,7.16,14.31l4.42-2.21V152A8,8,0,0,0,136,160Z"></path></svg>
|
||||
|
|
Before (image error) Size: 627 B After (image error) Size: 627 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M232,71.84V184.16a15.92,15.92,0,0,1-24.48,13.34L128,146.86v37.3a15.92,15.92,0,0,1-24.48,13.34L15.33,141.34a15.8,15.8,0,0,1,0-26.68L103.52,58.5A15.91,15.91,0,0,1,128,71.84v37.3L207.52,58.5A15.91,15.91,0,0,1,232,71.84Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M232,71.84V184.16a15.92,15.92,0,0,1-24.48,13.34L128,146.86v37.3a15.92,15.92,0,0,1-24.48,13.34L15.33,141.34a15.8,15.8,0,0,1,0-26.68L103.52,58.5A15.91,15.91,0,0,1,128,71.84v37.3L207.52,58.5A15.91,15.91,0,0,1,232,71.84Z"></path></svg>
|
||||
|
|
Before (image error) Size: 415 B After (image error) Size: 415 B |
|
@ -2,7 +2,7 @@
|
|||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
fill="#ffffff"
|
||||
fill="#f0f0f0"
|
||||
viewBox="0 0 256 256"
|
||||
data-darkreader-inline-fill=""
|
||||
version="1.1"
|
||||
|
|
Before (image error) Size: 847 B After (image error) Size: 847 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M237.66,178.34a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,200,208V192a72.15,72.15,0,0,1-57.65-30.14l-41.72-58.4A56.1,56.1,0,0,0,55.06,80H32a8,8,0,0,1,0-16H55.06a72.12,72.12,0,0,1,58.59,30.15l41.72,58.4A56.08,56.08,0,0,0,200,176V160a8,8,0,0,1,13.66-5.66ZM143,107a8,8,0,0,0,11.16-1.86l1.2-1.67A56.08,56.08,0,0,1,200,80V96a8,8,0,0,0,13.66,5.66l24-24a8,8,0,0,0,0-11.32l-24-24A8,8,0,0,0,200,48V64a72.15,72.15,0,0,0-57.65,30.14l-1.2,1.67A8,8,0,0,0,143,107Zm-30,42a8,8,0,0,0-11.16,1.86l-1.2,1.67A56.1,56.1,0,0,1,55.06,176H32a8,8,0,0,0,0,16H55.06a72.12,72.12,0,0,0,58.59-30.15l1.2-1.67A8,8,0,0,0,113,149Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#f0f0f0" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M237.66,178.34a8,8,0,0,1,0,11.32l-24,24A8,8,0,0,1,200,208V192a72.15,72.15,0,0,1-57.65-30.14l-41.72-58.4A56.1,56.1,0,0,0,55.06,80H32a8,8,0,0,1,0-16H55.06a72.12,72.12,0,0,1,58.59,30.15l41.72,58.4A56.08,56.08,0,0,0,200,176V160a8,8,0,0,1,13.66-5.66ZM143,107a8,8,0,0,0,11.16-1.86l1.2-1.67A56.08,56.08,0,0,1,200,80V96a8,8,0,0,0,13.66,5.66l24-24a8,8,0,0,0,0-11.32l-24-24A8,8,0,0,0,200,48V64a72.15,72.15,0,0,0-57.65,30.14l-1.2,1.67A8,8,0,0,0,143,107Zm-30,42a8,8,0,0,0-11.16,1.86l-1.2,1.67A56.1,56.1,0,0,1,55.06,176H32a8,8,0,0,0,0,16H55.06a72.12,72.12,0,0,0,58.59-30.15l1.2-1.67A8,8,0,0,0,113,149Z"></path></svg>
|
||||
|
|
Before (image error) Size: 787 B After (image error) Size: 787 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M208,47.88V208.12a16,16,0,0,1-24.43,13.43L64,146.77V216a8,8,0,0,1-16,0V40a8,8,0,0,1,16,0v69.23L183.57,34.45A15.95,15.95,0,0,1,208,47.88Z"></path></svg>
|
Before (image error) Size: 335 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M208,40V216a8,8,0,0,1-16,0V146.77L72.43,221.55A15.95,15.95,0,0,1,48,208.12V47.88A15.95,15.95,0,0,1,72.43,34.45L192,109.23V40a8,8,0,0,1,16,0Z"></path></svg>
|
Before (image error) Size: 339 B |
281
modules/user/modules/quickshell/shell/launcher/Controller.qml
Normal file
|
@ -0,0 +1,281 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.SystemTray
|
||||
import ".."
|
||||
|
||||
Singleton {
|
||||
PersistentProperties {
|
||||
id: persist
|
||||
property bool launcherOpen: false;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "launcher"
|
||||
|
||||
function open(): void {
|
||||
persist.launcherOpen = true;
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
persist.launcherOpen = false;
|
||||
}
|
||||
|
||||
function toggle(): void {
|
||||
persist.launcherOpen = !persist.launcherOpen
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: loader
|
||||
activeAsync: persist.launcherOpen
|
||||
|
||||
PanelWindow {
|
||||
width: 450
|
||||
height: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10
|
||||
color: "transparent"
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
WlrLayershell.namespace: "shell:launcher"
|
||||
|
||||
Rectangle {
|
||||
//anchors.fill: 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: ShellGlobals.colors.bar
|
||||
radius: 5
|
||||
border.color: ShellGlobals.colors.barOutline
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 7
|
||||
anchors.bottomMargin: 0
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
id: searchContainer
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: searchbox.implicitHeight + 10
|
||||
color: "#30c0ffff"
|
||||
radius: 3
|
||||
border.color: "#50ffffff"
|
||||
|
||||
RowLayout {
|
||||
id: searchbox
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
|
||||
IconImage {
|
||||
implicitSize: parent.height
|
||||
source: "root:icons/magnifying-glass.svg"
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: search
|
||||
Layout.fillWidth: true
|
||||
color: "white"
|
||||
|
||||
focus: true
|
||||
Keys.forwardTo: [list]
|
||||
Keys.onEscapePressed: persist.launcherOpen = 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) => {
|
||||
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 {
|
||||
radius: 5
|
||||
color: "#20e0ffff"
|
||||
border.color: "#30ffffff"
|
||||
border.width: 1
|
||||
}
|
||||
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: {
|
||||
modelData.execute();
|
||||
persist.launcherOpen = 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
|
||||
color: "#f0f0f0"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {}
|
||||
}
|
|
@ -3,8 +3,10 @@ pragma Singleton
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Pam
|
||||
import ".."
|
||||
import "../.."
|
||||
|
||||
|
@ -61,18 +63,15 @@ Singleton {
|
|||
root.oldWorkspaces = ({});
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
name: "lock"
|
||||
onPressed: {
|
||||
if (root.locked) root.locked = false;
|
||||
else root.locked = true;
|
||||
}
|
||||
IpcHandler {
|
||||
target: "lockscreen"
|
||||
function lock(): void { root.locked = true; }
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: lockContextLoader
|
||||
|
||||
LockContext {
|
||||
SessionLockContext {
|
||||
onUnlocked: root.locked = false;
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +81,7 @@ Singleton {
|
|||
|
||||
onSecureChanged: {
|
||||
if (secure) {
|
||||
Qt.callLater(() => root.workspaceLockAnimation());
|
||||
root.workspaceLockAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,7 +107,7 @@ Singleton {
|
|||
|
||||
LockContent {
|
||||
id: lockContent
|
||||
context: lockContextLoader.item;
|
||||
state: lockContextLoader.item.state;
|
||||
|
||||
visible: false
|
||||
width: lockSurface.width
|
||||
|
@ -128,7 +127,6 @@ Singleton {
|
|||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
lockContent.y = -lockSurface.height
|
||||
console.log(`y ${lockContent.y}`)
|
||||
lockContent.visible = true;
|
||||
lockAnim.running = true;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Greetd
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
signal launch();
|
||||
|
||||
property LockState state: LockState {
|
||||
onTryPasswordUnlock: {
|
||||
this.isUnlocking = true;
|
||||
Greetd.createSession("admin");
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Greetd
|
||||
|
||||
function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) {
|
||||
if (responseRequired) {
|
||||
Greetd.respond(root.state.currentText);
|
||||
} // else ignore - only supporting passwords
|
||||
}
|
||||
|
||||
function onAuthFailure() {
|
||||
root.state.currentText = "";
|
||||
root.state.error = "Invalid password";
|
||||
root.state.failed = true;
|
||||
root.state.isUnlocking = false;
|
||||
}
|
||||
|
||||
function onReadyToLaunch() {
|
||||
root.state.isUnlocking = false;
|
||||
root.launch();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,6 +44,8 @@ Item {
|
|||
anchors.margins: 15
|
||||
|
||||
source: root.icon
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,113 +1,202 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import ".."
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property LockContext context;
|
||||
required property LockState state;
|
||||
|
||||
property real focusAnim: focusAnimInternal * 0.001
|
||||
property int focusAnimInternal: Window.active ? 1000 : 0
|
||||
Behavior on focusAnimInternal { SmoothedAnimation { velocity: 5000 } }
|
||||
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: parent.height / 2 + textBox.height
|
||||
id: sep
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
implicitHeight: 6
|
||||
implicitWidth: 800
|
||||
radius: height / 2
|
||||
color: ShellGlobals.colors.widget
|
||||
}
|
||||
property real startMoveX: 0
|
||||
property real startMoveY: 0
|
||||
|
||||
ColumnLayout {
|
||||
implicitWidth: sep.implicitWidth
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: sep.top
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
id: timeText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
font {
|
||||
pointSize: 120
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
family: "Noto Sans"
|
||||
}
|
||||
|
||||
color: "white"
|
||||
renderType: Text.NativeRendering
|
||||
|
||||
text: {
|
||||
const hours = ShellGlobals.time.getHours().toString().padStart(2, '0');
|
||||
const minutes = ShellGlobals.time.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
// prevents wakeups from bumping the mouse
|
||||
onPositionChanged: event => {
|
||||
if (root.state.fadedOut) {
|
||||
if (root.state.mouseMoved()) {
|
||||
const xOffset = Math.abs(event.x - startMoveX);
|
||||
const yOffset = Math.abs(event.y - startMoveY);
|
||||
const distanceSq = (xOffset * xOffset) + (yOffset * yOffset);
|
||||
if (distanceSq > (100 * 100)) root.state.fadeIn();
|
||||
} else {
|
||||
startMoveX = event.x;
|
||||
startMoveY = event.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitHeight: childrenRect.height * focusAnim
|
||||
implicitWidth: sep.implicitWidth
|
||||
clip: true
|
||||
id: content
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
y: root.state.fadeOutMul * (height / 2 + childrenRect.height)
|
||||
|
||||
TextInput {
|
||||
id: textBox
|
||||
focus: true
|
||||
width: parent.width
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: parent.height / 2 + textBox.height
|
||||
id: sep
|
||||
|
||||
color: enabled ?
|
||||
root.context.failed ? "#ffa0a0" : "white"
|
||||
: "#80ffffff";
|
||||
implicitHeight: 6
|
||||
implicitWidth: 800
|
||||
radius: height / 2
|
||||
color: ShellGlobals.colors.widget
|
||||
}
|
||||
|
||||
font.pointSize: 24
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
ColumnLayout {
|
||||
implicitWidth: sep.implicitWidth
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: sep.top
|
||||
spacing: 0
|
||||
|
||||
onTextChanged: root.context.currentText = text;
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
|
||||
Window.onActiveChanged: {
|
||||
if (Window.active) {
|
||||
text = root.context.currentText;
|
||||
Text {
|
||||
id: timeText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
font {
|
||||
pointSize: 120
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
family: "Noto Sans"
|
||||
}
|
||||
|
||||
color: "white"
|
||||
renderType: Text.NativeRendering
|
||||
|
||||
text: {
|
||||
const hours = clock.hours.toString().padStart(2, '0');
|
||||
const minutes = clock.minutes.toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
if (text != "") root.context.tryUnlock();
|
||||
}
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitHeight: textBox.height * focusAnim
|
||||
implicitWidth: sep.implicitWidth
|
||||
clip: true
|
||||
|
||||
enabled: !root.context.isUnlocking;
|
||||
TextInput {
|
||||
id: textBox
|
||||
focus: true
|
||||
width: parent.width
|
||||
|
||||
color: enabled ?
|
||||
root.state.failed ? "#ffa0a0" : "white"
|
||||
: "#80ffffff";
|
||||
|
||||
font.pointSize: 24
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
|
||||
cursorVisible: text != ""
|
||||
onCursorVisibleChanged: cursorVisible = text != ""
|
||||
|
||||
onTextChanged: {
|
||||
root.state.currentText = text;
|
||||
cursorVisible = text != ""
|
||||
}
|
||||
|
||||
Window.onActiveChanged: {
|
||||
if (Window.active) {
|
||||
text = root.state.currentText;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.state
|
||||
|
||||
function onCurrentTextChanged() {
|
||||
textBox.text = root.state.currentText;
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
if (text != "") root.state.tryPasswordUnlock();
|
||||
}
|
||||
|
||||
enabled: !root.state.isUnlocking;
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: textBox
|
||||
font: textBox.font
|
||||
color: root.state.failed ? "#ffa0a0" : "#80ffffff";
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
visible: !textBox.cursorVisible
|
||||
text: root.state.failed ? root.state.error
|
||||
: root.state.fprintAvailable ? "Touch sensor or enter password" : "Enter password";
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: height
|
||||
color: "transparent"
|
||||
visible: root.state.fprintAvailable
|
||||
|
||||
anchors {
|
||||
right: textBox.right
|
||||
top: textBox.top
|
||||
bottom: textBox.bottom
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
source: "root:icons/fingerprint.svg"
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: sep.bottom
|
||||
implicitHeight: (75 + 30) * focusAnim
|
||||
implicitWidth: sep.implicitWidth
|
||||
clip: true
|
||||
|
||||
RowLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 50
|
||||
spacing: 0
|
||||
|
||||
LockButton {
|
||||
icon: "root:icons/monitor.svg"
|
||||
onClicked: root.state.fadeOut();
|
||||
}
|
||||
|
||||
LockButton {
|
||||
icon: "root:icons/pause.svg"
|
||||
show: root.state.mediaPlaying;
|
||||
onClicked: root.state.pauseMedia();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: sep.bottom
|
||||
implicitHeight: (childrenRect.height + 30) * focusAnim
|
||||
implicitWidth: sep.implicitWidth
|
||||
clip: true
|
||||
|
||||
RowLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 50
|
||||
spacing: 0
|
||||
|
||||
LockButton {
|
||||
icon: "root:icons/monitor.svg"
|
||||
onClicked: root.context.dpms();
|
||||
}
|
||||
|
||||
LockButton {
|
||||
icon: "root:icons/pause.svg"
|
||||
show: context.mediaPlaying;
|
||||
onClicked: root.context.pauseMedia();
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: darkenOverlay
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: root.state.fadeOutMul
|
||||
visible: opacity != 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
signal unlocked();
|
||||
property string currentText: "";
|
||||
readonly property alias isUnlocking: pamtester.running;
|
||||
property bool failed: false;
|
||||
|
||||
onCurrentTextChanged: failed = false;
|
||||
|
||||
readonly property bool mediaPlaying: Mpris.players.values.some(player => {
|
||||
return player.playbackState === MprisPlaybackState.Playing && player.canPause;
|
||||
});
|
||||
|
||||
function pauseMedia() {
|
||||
Mpris.players.values.forEach(player => {
|
||||
if (player.playbackState === MprisPlaybackState.Playing && player.canPause) {
|
||||
player.playbackState = MprisPlaybackState.Paused;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function dpms() {
|
||||
Hyprland.dispatch(`dpms`);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: pamtester
|
||||
property bool failed: true
|
||||
|
||||
command: ["pamtester", "login", Quickshell.env("USER"), "authenticate"]
|
||||
|
||||
onStarted: this.write(`${currentText}\n`)
|
||||
|
||||
stdout: SplitParser {
|
||||
// fails go to stderr
|
||||
onRead: pamtester.failed = false
|
||||
}
|
||||
|
||||
onExited: {
|
||||
if (failed) {
|
||||
root.failed = true;
|
||||
} else {
|
||||
root.unlocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryUnlock() {
|
||||
pamtester.running = true;
|
||||
}
|
||||
}
|
70
modules/user/modules/quickshell/shell/lock/LockState.qml
Normal file
|
@ -0,0 +1,70 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Scope {
|
||||
signal tryPasswordUnlock();
|
||||
property string currentText: "";
|
||||
property string error: "";
|
||||
property bool isUnlocking: false;
|
||||
property bool failed: false;
|
||||
property bool fprintAvailable: false;
|
||||
|
||||
property bool fadedOut: false
|
||||
property real fadeOutMul: 0
|
||||
|
||||
NumberAnimation on fadeOutMul {
|
||||
id: fadeAnim
|
||||
duration: 600
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: [0.0, 0.75, 0.15, 1.0, 1.0, 1.0]
|
||||
|
||||
onStopped: {
|
||||
if (fadedOut) Hyprland.dispatch("dpms off");
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentTextChanged: {
|
||||
failed = false;
|
||||
error = "";
|
||||
|
||||
if (fadedOut) {
|
||||
fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function fadeOut() {
|
||||
if (fadedOut) return;
|
||||
fadedOut = true;
|
||||
fadeAnim.to = 1;
|
||||
fadeAnim.restart();
|
||||
}
|
||||
|
||||
function fadeIn() {
|
||||
if (!fadedOut) return;
|
||||
Hyprland.dispatch("dpms on");
|
||||
fadedOut = false;
|
||||
fadeAnim.to = 0;
|
||||
fadeAnim.restart();
|
||||
}
|
||||
|
||||
ElapsedTimer { id: mouseTimer }
|
||||
|
||||
// returns if mouse move should be continued, false should restart
|
||||
function mouseMoved(): bool {
|
||||
return mouseTimer.restart() < 0.2;
|
||||
}
|
||||
|
||||
readonly property bool mediaPlaying: Mpris.players.values.some(player => {
|
||||
return player.playbackState === MprisPlaybackState.Playing && player.canPause;
|
||||
});
|
||||
|
||||
function pauseMedia() {
|
||||
Mpris.players.values.forEach(player => {
|
||||
if (player.playbackState === MprisPlaybackState.Playing && player.canPause) {
|
||||
player.playbackState = MprisPlaybackState.Paused;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import Quickshell
|
||||
import Quickshell.Services.Pam
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
signal unlocked();
|
||||
|
||||
property LockState state: LockState {
|
||||
onTryPasswordUnlock: {
|
||||
root.state.isUnlocking = true;
|
||||
pam.start();
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: pam
|
||||
configDirectory: "pam"
|
||||
config: "password.conf"
|
||||
|
||||
onPamMessage: {
|
||||
if (this.responseRequired) {
|
||||
this.respond(root.state.currentText);
|
||||
} else if (this.messageIsError) {
|
||||
root.state.currentText = "";
|
||||
root.state.failed = true;
|
||||
root.state.error = this.message;
|
||||
} // else ignore
|
||||
}
|
||||
|
||||
onCompleted: status => {
|
||||
const success = status == PamResult.Success;
|
||||
|
||||
if (!success) {
|
||||
root.state.currentText = "";
|
||||
root.state.error = "Invalid password";
|
||||
}
|
||||
|
||||
root.state.failed = !success;
|
||||
root.state.isUnlocking = false;
|
||||
|
||||
if (success) root.unlocked();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
auth required /run/current-system/sw/lib/security/pam_fprintd.so
|
|
@ -0,0 +1 @@
|
|||
auth required pam_unix.so
|
|
@ -0,0 +1,39 @@
|
|||
import QtQuick
|
||||
|
||||
Canvas {
|
||||
id: root
|
||||
property real ringFill: 1.0
|
||||
|
||||
onRingFillChanged: requestPaint();
|
||||
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = "#70ffffff";
|
||||
|
||||
ctx.beginPath();
|
||||
const half = Math.round(root.width / 2);
|
||||
const start = -Math.PI * 0.5;
|
||||
const endM = ringFill == 0.0 || ringFill == 1.0 ? ringFill : 1.0 - ringFill
|
||||
ctx.arc(half, half, half - ctx.lineWidth, start, start + 2 * Math.PI * endM, true);
|
||||
ctx.stroke();
|
||||
|
||||
const xMin = Math.min(root.width * 0.3);
|
||||
const xMax = Math.max(root.width * 0.7);
|
||||
ctx.strokeStyle = "white";
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xMin, xMin);
|
||||
ctx.lineTo(xMax, xMax);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xMax, xMin);
|
||||
ctx.lineTo(xMin, xMax);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
TrackedNotification {
|
||||
id: root
|
||||
required property Notification notif;
|
||||
|
||||
renderComponent: StandardNotificationRenderer {
|
||||
notif: root.notif
|
||||
backer: root
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
if (!lock.retained) notif.dismiss();
|
||||
root.discarded();
|
||||
}
|
||||
|
||||
function handleDismiss() {
|
||||
//handleDiscard();
|
||||
}
|
||||
|
||||
RetainableLock {
|
||||
id: lock
|
||||
object: root.notif
|
||||
locked: true
|
||||
onRetainedChanged: {
|
||||
if (retained) root.discard();
|
||||
}
|
||||
}
|
||||
|
||||
expireTimeout: notif.expireTimeout
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "../components"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
enum FlingState {
|
||||
Inert,
|
||||
Returning,
|
||||
Flinging,
|
||||
Dismissing
|
||||
}
|
||||
|
||||
implicitWidth: display.implicitWidth
|
||||
|
||||
// note: can be 0, use ZHVStack
|
||||
implicitHeight: (display.implicitHeight + padding * 2) * display.meshFactor
|
||||
|
||||
z: 1.0 - display.meshFactor
|
||||
|
||||
property var view;
|
||||
property Item contentItem;
|
||||
property real padding: 5;
|
||||
property real edgeXOffset;
|
||||
property bool canOverlap: display.rotation > 2 || Math.abs(display.displayY) > 10 || display.displayX < -60
|
||||
property bool canDismiss: display.state != FlickableNotification.Dismissing && display.state != FlickableNotification.Flinging;
|
||||
|
||||
property alias displayContainer: displayContainer;
|
||||
|
||||
signal leftViewBounds();
|
||||
signal dismissed();
|
||||
signal discarded();
|
||||
signal startedFlick();
|
||||
|
||||
function playEntry(delay: real) {
|
||||
if (display.state != FlickableNotification.Flinging) {
|
||||
display.displayX = -display.width + edgeXOffset
|
||||
root.playReturn(delay);
|
||||
}
|
||||
}
|
||||
|
||||
function playDismiss(delay: real) {
|
||||
if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
|
||||
display.state = FlickableNotification.Dismissing;
|
||||
display.animationDelay = delay;
|
||||
}
|
||||
}
|
||||
|
||||
function playDiscard(delay: real) {
|
||||
if (display.state != FlickableNotification.Flinging && display.state != FlickableNotification.Dismissing) {
|
||||
display.velocityX = 500;
|
||||
display.velocityY = 1500;
|
||||
display.state = FlickableNotification.Flinging;
|
||||
display.animationDelay = delay;
|
||||
}
|
||||
}
|
||||
|
||||
function playReturn(delay: real) {
|
||||
if (display.state != FlickableNotification.Flinging) {
|
||||
display.state = FlickableNotification.Returning;
|
||||
display.animationDelay = delay;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
width: display.width
|
||||
height: display.height
|
||||
enabled: display.state == FlickableNotification.Inert || display.state == FlickableNotification.Returning
|
||||
|
||||
FlickMonitor {
|
||||
id: flickMonitor
|
||||
target: mouseArea
|
||||
|
||||
onDragDeltaXChanged: {
|
||||
const delta = dragDeltaX;
|
||||
display.displayX = delta < 0 ? delta : Math.pow(delta, 0.8);
|
||||
display.updateMeshFactor(true);
|
||||
updateDragY();
|
||||
}
|
||||
|
||||
onDragDeltaYChanged: {
|
||||
updateDragY();
|
||||
display.state = FlickableNotification.Inert;
|
||||
}
|
||||
|
||||
function updateDragY() {
|
||||
//const xMul = 1//dragDeltaX < 0 ? 0 : Math.min(1, Math.pow(dragDeltaX / 200, 0.8));
|
||||
const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
|
||||
const xMul = d
|
||||
const targetY = dragDeltaY;
|
||||
display.displayY = root.padding + targetY * xMul;
|
||||
}
|
||||
|
||||
onFlickStarted: {
|
||||
display.initialAnimComplete = true;
|
||||
root.startedFlick();
|
||||
}
|
||||
|
||||
onFlickCompleted: {
|
||||
display.releaseY = dragEndY;
|
||||
|
||||
if (velocityX > 1000 || (velocityX > -100 && display.displayX > display.width * 0.4)) {
|
||||
display.velocityX = Math.max(velocityX * 0.8, 1000);
|
||||
display.velocityY = velocityY * 0.6;
|
||||
display.state = FlickableNotification.Flinging;
|
||||
root.discarded();
|
||||
} else if (velocityX < -1500 || (velocityX < 100 && display.displayX < -(display.width * 0.4))) {
|
||||
display.velocityX = Math.min(velocityX * 0.8, -700)
|
||||
display.velocityY = 0
|
||||
display.state = FlickableNotification.Dismissing;
|
||||
root.dismissed();
|
||||
} else {
|
||||
display.velocityX = 0;
|
||||
display.velocityY = 0;
|
||||
display.state = FlickableNotification.Returning;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: displayContainer
|
||||
layer.enabled: view && view.topNotification == root
|
||||
opacity: layer.enabled ? 0 : 1 // shader ignores it
|
||||
width: Math.ceil(display.width + display.xPadding * 2)
|
||||
height: Math.ceil(display.height + display.yPadding * 2)
|
||||
|
||||
x: Math.floor(display.targetContainmentX)
|
||||
y: Math.floor(display.targetContainmentY)
|
||||
|
||||
Item {
|
||||
id: display
|
||||
//anchors.centerIn: parent
|
||||
x: xPadding + (targetContainmentX - displayContainer.x)
|
||||
y: yPadding + (targetContainmentY - displayContainer.y)
|
||||
//visible: meshFactor > 0.95
|
||||
|
||||
children: [root.contentItem]
|
||||
implicitWidth: root.contentItem?.width ?? 0
|
||||
implicitHeight: root.contentItem?.height ?? 0
|
||||
|
||||
property var state: FlickableNotification.Inert;
|
||||
property real meshFactor: 1;
|
||||
property real velocityX;
|
||||
property real velocityY;
|
||||
property real releaseY;
|
||||
property real animationDelay;
|
||||
property bool initialAnimComplete;
|
||||
|
||||
property real displayX;
|
||||
property real displayY;
|
||||
|
||||
property real tiltSize: Math.max(width, height) * 1.2;
|
||||
property real xPadding: (tiltSize - width) / 2;
|
||||
property real yPadding: (tiltSize - height) / 2;
|
||||
|
||||
property real targetContainmentX: display.displayX - display.xPadding
|
||||
property real targetContainmentY: root.padding + display.displayY - display.yPadding
|
||||
|
||||
function updateMeshFactor(canRemesh: bool) {
|
||||
let meshFactor = (display.implicitWidth - Math.abs(display.displayX)) / display.implicitWidth;
|
||||
meshFactor = 0.8 + (meshFactor * 0.2);
|
||||
meshFactor = Math.max(0, meshFactor);
|
||||
|
||||
if (canRemesh) this.meshFactor = meshFactor;
|
||||
else this.meshFactor = Math.min(this.meshFactor, meshFactor);
|
||||
}
|
||||
|
||||
function unmesh(delta: real) {
|
||||
if (meshFactor > 0) {
|
||||
this.meshFactor = Math.max(0, this.meshFactor - delta * 5);
|
||||
}
|
||||
}
|
||||
|
||||
rotation: display.displayX < 0 ? 0 : display.displayX * (initialAnimComplete ? 0.1 : 0.02)
|
||||
|
||||
property real lastX;
|
||||
|
||||
FrameAnimation {
|
||||
function dampingVelocity(currentVelocity, delta) {
|
||||
const spring = 1.0;
|
||||
const damping = 0.1;
|
||||
const springForce = spring * delta;
|
||||
const dampingForce = -damping * currentVelocity;
|
||||
return currentVelocity + (springForce + dampingForce);
|
||||
}
|
||||
|
||||
running: display.state != FlickableNotification.Inert
|
||||
onTriggered: {
|
||||
let frameTime = this.frameTime;
|
||||
if (display.animationDelay != 0) {
|
||||
const usedDelay = Math.min(display.animationDelay, frameTime);
|
||||
frameTime -= usedDelay;
|
||||
display.animationDelay -= usedDelay;
|
||||
if (frameTime == 0) return;
|
||||
}
|
||||
|
||||
if (display.state == FlickableNotification.Flinging) {
|
||||
display.velocityY += frameTime * 100000 * (1 / display.velocityX * 100);
|
||||
//display.velocityX -= display.velocityX * 0.98 * frameTime
|
||||
display.unmesh(frameTime);
|
||||
} else if (display.state == FlickableNotification.Dismissing) {
|
||||
const d = Math.max(0, Math.min(5000, display.displayX)) / 2000;
|
||||
display.displayY = root.padding + display.releaseY * d;
|
||||
display.velocityY = 0;
|
||||
|
||||
display.velocityX += frameTime * -20000;
|
||||
|
||||
if (display.displayX + display.width > 0) display.updateMeshFactor(false);
|
||||
else display.unmesh(frameTime);
|
||||
} else {
|
||||
const deltaX = 0 - display.displayX;
|
||||
const deltaY = root.padding - display.displayY;
|
||||
|
||||
display.velocityX = dampingVelocity(display.velocityX, deltaX);
|
||||
display.velocityY = dampingVelocity(display.velocityY, deltaY);
|
||||
|
||||
if (Math.abs(display.velocityX) < 0.01 && Math.abs(deltaX) < 1
|
||||
&& Math.abs(display.velocityY) < 0.01 && Math.abs(deltaY) < 1) {
|
||||
display.state = FlickableNotification.Inert;
|
||||
display.displayX = 0;
|
||||
display.displayY = root.padding;
|
||||
display.velocityX = 0;
|
||||
display.velocityY = 0;
|
||||
display.initialAnimComplete = true;
|
||||
}
|
||||
|
||||
display.updateMeshFactor(true);
|
||||
}
|
||||
|
||||
display.displayX += display.velocityX * frameTime;
|
||||
display.displayY += display.velocityY * frameTime;
|
||||
|
||||
|
||||
// todo: actually base this on the viewport
|
||||
if (display.displayX > 10000 || display.displayY > 10000 || (display.displayX + display.width < root.edgeXOffset && display.meshFactor == 0) || display.displayY < -10000) root.leftViewBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "../components"
|
||||
import "../shaders" as Shaders
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property list<Item> notifications: [];
|
||||
property list<Item> heightStack: [];
|
||||
|
||||
property alias stack: stack;
|
||||
property alias topNotification: stack.topNotification;
|
||||
|
||||
function addNotificationInert(notification: TrackedNotification): Item {
|
||||
const harness = stack._harnessComponent.createObject(stack, {
|
||||
backer: notification,
|
||||
view: root,
|
||||
});
|
||||
|
||||
harness.contentItem = notification.renderComponent.createObject(harness);
|
||||
|
||||
notifications = [...notifications, harness];
|
||||
heightStack = [harness, ...heightStack];
|
||||
|
||||
return harness;
|
||||
}
|
||||
|
||||
function addNotification(notification: TrackedNotification) {
|
||||
const harness = root.addNotificationInert(notification);
|
||||
harness.playEntry(0);
|
||||
}
|
||||
|
||||
function dismissAll() {
|
||||
let delay = 0;
|
||||
|
||||
for (const notification of root.notifications) {
|
||||
if (!notification.canDismiss) continue;
|
||||
notification.playDismiss(delay);
|
||||
notification.dismissed();
|
||||
delay += 0.025;
|
||||
}
|
||||
}
|
||||
|
||||
function discardAll() {
|
||||
let delay = 0;
|
||||
|
||||
for (const notification of root.notifications) {
|
||||
if (!notification.canDismiss) continue;
|
||||
notification.playDismiss(delay);
|
||||
notification.discarded();
|
||||
delay += 0.025;
|
||||
}
|
||||
}
|
||||
|
||||
function addSet(notifications: list<TrackedNotification>) {
|
||||
let delay = 0;
|
||||
|
||||
for (const notification of notifications) {
|
||||
if (notification.visualizer) {
|
||||
notification.visualizer.playReturn(delay);
|
||||
} else {
|
||||
const harness = root.addNotificationInert(notification);
|
||||
harness.playEntry(delay);
|
||||
}
|
||||
|
||||
delay += 0.025;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
layer.enabled: stack.topNotification != null
|
||||
layer.effect: Shaders.MaskedOverlay {
|
||||
overlayItem: stack.topNotification?.displayContainer ?? null
|
||||
overlayPos: Qt.point(stack.x + stack.topNotification.x + overlayItem.x, stack.y + stack.topNotification.y + overlayItem.y)
|
||||
}
|
||||
|
||||
ZHVStack {
|
||||
id: stack
|
||||
|
||||
property Item topNotification: {
|
||||
if (root.heightStack.length < 2) return null;
|
||||
const top = root.heightStack[0] ?? null;
|
||||
return top && top.canOverlap ? top : null;
|
||||
};
|
||||
|
||||
property Component _harnessComponent: FlickableNotification {
|
||||
id: notification
|
||||
required property TrackedNotification backer;
|
||||
|
||||
edgeXOffset: -stack.x
|
||||
|
||||
onDismissed: backer.handleDismiss();
|
||||
onDiscarded: backer.handleDiscard();
|
||||
|
||||
onLeftViewBounds: {
|
||||
root.notifications = root.notifications.filter(n => n != this);
|
||||
root.heightStack = root.heightStack.filter(n => n != this);
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
onStartedFlick: {
|
||||
root.heightStack = [this, ...root.heightStack.filter(n => n != this)];
|
||||
}
|
||||
|
||||
Component.onCompleted: backer.visualizer = this;
|
||||
|
||||
Connections {
|
||||
target: backer
|
||||
|
||||
function onDismiss() {
|
||||
notification.playDismiss(0);
|
||||
notification.dismissed();
|
||||
}
|
||||
|
||||
function onDiscard() {
|
||||
notification.playDismiss(0);
|
||||
notification.discarded();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<TrackedNotification> notifications;
|
||||
property Component notifComponent: DaemonNotification {}
|
||||
|
||||
property bool showTrayNotifs: false;
|
||||
property bool dnd: false;
|
||||
property bool hasNotifs: root.notifications.length != 0
|
||||
property var lastHoveredNotif;
|
||||
|
||||
property var overlay;
|
||||
|
||||
signal notif(notif: TrackedNotification);
|
||||
signal showAll(notifications: list<TrackedNotification>);
|
||||
signal dismissAll(notifications: list<TrackedNotification>);
|
||||
signal discardAll(notifications: list<TrackedNotification>);
|
||||
|
||||
NotificationServer {
|
||||
imageSupported: true
|
||||
actionsSupported: true
|
||||
actionIconsSupported: true
|
||||
|
||||
onNotification: notification => {
|
||||
notification.tracked = true;
|
||||
|
||||
const notif = root.notifComponent.createObject(null, { notif: notification });
|
||||
root.notifications = [...root.notifications, notif];
|
||||
|
||||
root.notif(notif);
|
||||
}
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: root.notifications
|
||||
|
||||
Connections {
|
||||
required property TrackedNotification modelData;
|
||||
target: modelData;
|
||||
|
||||
function onDiscarded() {
|
||||
root.notifications = root.notifications.filter(n => n != target);
|
||||
modelData.untrack();
|
||||
}
|
||||
|
||||
function onDiscard() {
|
||||
if (!modelData.visualizer) modelData.discarded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onShowTrayNotifsChanged: {
|
||||
if (showTrayNotifs) {
|
||||
for (const notif of root.notifications) {
|
||||
notif.inTray = true;
|
||||
}
|
||||
|
||||
root.showAll(root.notifications);
|
||||
} else {
|
||||
root.dismissAll(root.notifications);
|
||||
}
|
||||
}
|
||||
|
||||
function sendDiscardAll() {
|
||||
root.discardAll(root.notifications);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
PanelWindow {
|
||||
WlrLayershell.namespace: "shell:notifications"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
bottom: true
|
||||
right: true
|
||||
}
|
||||
|
||||
property Component notifComponent: DaemonNotification {}
|
||||
|
||||
NotificationDisplay {
|
||||
id: display
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
stack.y: 5 + 55//(NotificationManager.showTrayNotifs ? 55 : 0)
|
||||
stack.x: 72
|
||||
}
|
||||
|
||||
visible: display.stack.children.length != 0
|
||||
|
||||
mask: Region { item: display.stack }
|
||||
|
||||
Component.onCompleted: {
|
||||
NotificationManager.overlay = this;
|
||||
NotificationManager.notif.connect(display.addNotification);
|
||||
NotificationManager.showAll.connect(display.addSet);
|
||||
NotificationManager.dismissAll.connect(display.dismissAll);
|
||||
NotificationManager.discardAll.connect(display.discardAll);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:bar"
|
||||
|
||||
BarWidgetInner {
|
||||
id: root
|
||||
required property var bar;
|
||||
|
||||
property bool controlsOpen: false;
|
||||
onControlsOpenChanged: NotificationManager.showTrayNotifs = controlsOpen;
|
||||
|
||||
Connections {
|
||||
target: NotificationManager
|
||||
|
||||
function onHasNotifsChanged() {
|
||||
if (!NotificationManager.hasNotifs) {
|
||||
root.controlsOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: width
|
||||
|
||||
BarButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
baseMargin: 8
|
||||
fillWindowWidth: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
showPressed: root.controlsOpen || (pressedButtons & ~Qt.RightButton)
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
|
||||
source: NotificationManager.hasNotifs
|
||||
? "root:icons/bell-fill.svg"
|
||||
: "root:icons/bell.svg"
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
|
||||
onPressed: event => {
|
||||
if (event.button == Qt.RightButton && NotificationManager.hasNotifs) {
|
||||
root.controlsOpen = !root.controlsOpen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var tooltip: TooltipItem {
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
show: button.containsMouse
|
||||
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
const count = NotificationManager.notifications.length;
|
||||
return count == 0 ? "No notifications"
|
||||
: count == 1 ? "1 notification"
|
||||
: `${count} notifications`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var rightclickMenu: TooltipItem {
|
||||
tooltip: bar.tooltip
|
||||
owner: root
|
||||
isMenu: true
|
||||
grabWindows: [NotificationManager.overlay]
|
||||
show: root.controlsOpen
|
||||
onClose: root.controlsOpen = false
|
||||
|
||||
Item {
|
||||
implicitWidth: 440
|
||||
implicitHeight: root.implicitHeight - 10
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
implicitWidth: 30
|
||||
implicitHeight: 30
|
||||
|
||||
hoverEnabled: true
|
||||
onPressed: {
|
||||
NotificationManager.sendDiscardAll()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
radius: width * 0.5
|
||||
antialiasing: true
|
||||
color: "#60ffffff"
|
||||
opacity: closeArea.containsMouse ? 1 : 0
|
||||
Behavior on opacity { SmoothedAnimation { velocity: 8 } }
|
||||
}
|
||||
|
||||
CloseButton {
|
||||
anchors.fill: parent
|
||||
ringFill: root.backer.timePercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import ".."
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
required property Notification notif;
|
||||
required property var backer;
|
||||
|
||||
color: notif.urgency == NotificationUrgency.Critical ? "#30ff2030" : "#30c0ffff"
|
||||
radius: 5
|
||||
implicitWidth: 450
|
||||
implicitHeight: c.implicitHeight
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: {
|
||||
backer.pauseCounter += hovered ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: border
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.width: 2
|
||||
border.color: ShellGlobals.colors.widgetOutline
|
||||
radius: root.radius
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: c
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
ColumnLayout {
|
||||
Layout.margins: 10
|
||||
|
||||
RowLayout {
|
||||
Image {
|
||||
visible: source != ""
|
||||
source: notif.appIcon ? Quickshell.iconPath(notif.appIcon) : ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
antialiasing: true
|
||||
sourceSize.width: 30
|
||||
sourceSize.height: 30
|
||||
Layout.preferredWidth: 30
|
||||
Layout.preferredHeight: 30
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: text != ""
|
||||
text: notif.summary
|
||||
font.pointSize: 20
|
||||
elide: Text.ElideRight
|
||||
Layout.maximumWidth: root.implicitWidth - 100 // QTBUG-127649
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
Layout.preferredWidth: 30
|
||||
Layout.preferredHeight: 30
|
||||
|
||||
hoverEnabled: true
|
||||
onPressed: root.backer.discard();
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
radius: width * 0.5
|
||||
antialiasing: true
|
||||
color: "#60ffffff"
|
||||
opacity: closeArea.containsMouse ? 1 : 0
|
||||
Behavior on opacity { SmoothedAnimation { velocity: 8 } }
|
||||
}
|
||||
|
||||
CloseButton {
|
||||
anchors.fill: parent
|
||||
ringFill: root.backer.timePercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.topMargin: 3
|
||||
visible: bodyLabel.text != "" || notifImage.visible
|
||||
implicitWidth: bodyLabel.width
|
||||
implicitHeight: Math.max(notifImage.size, bodyLabel.implicitHeight)
|
||||
|
||||
Image {
|
||||
id: notifImage
|
||||
readonly property int size: visible ? 14 * 8 : 0
|
||||
y: bodyLabel.y + bodyLabel.topPadding
|
||||
|
||||
visible: source != ""
|
||||
source: notif.image
|
||||
fillMode: Image.PreserveAspectFit
|
||||
cache: false
|
||||
antialiasing: true
|
||||
|
||||
width: size
|
||||
height: size
|
||||
sourceSize.width: size
|
||||
sourceSize.height: size
|
||||
}
|
||||
|
||||
Label {
|
||||
id: bodyLabel
|
||||
width: root.implicitWidth - 20
|
||||
text: notif.body
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
onLineLaidOut: line => {
|
||||
if (!notifImage.visible) return;
|
||||
|
||||
const isize = notifImage.size + 6;
|
||||
if (line.y + line.height <= notifImage.y + isize) {
|
||||
line.x += isize;
|
||||
line.width -= isize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: root.border.width
|
||||
spacing: 0
|
||||
visible: notif.actions.length != 0
|
||||
|
||||
Rectangle {
|
||||
height: border.border.width
|
||||
Layout.fillWidth: true
|
||||
color: border.border.color
|
||||
antialiasing: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: notif.actions
|
||||
|
||||
Item {
|
||||
required property NotificationAction modelData;
|
||||
required property int index;
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 35
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
leftMargin: -implicitWidth * 0.5
|
||||
}
|
||||
|
||||
visible: index != 0
|
||||
implicitWidth: root.border.width
|
||||
color: ShellGlobals.colors.widgetOutline
|
||||
antialiasing: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionArea
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
modelData.invoke();
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: actionArea.pressed && actionArea.containsMouse ? "#20000000" : "transparent"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Image {
|
||||
visible: notif.hasActionIcons
|
||||
source: Quickshell.iconPath(modelData.identifier)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
antialiasing: true
|
||||
sourceSize.height: 25
|
||||
sourceSize.width: 25
|
||||
}
|
||||
|
||||
Label { text: modelData.text }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import QtQuick
|
||||
|
||||
TrackedNotification {
|
||||
renderComponent: StandardNotificationRenderer {}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
required property Component renderComponent;
|
||||
|
||||
property bool inTray: false;
|
||||
property bool destroyOnInvisible: false;
|
||||
property int visualizerCount: 0;
|
||||
property FlickableNotification visualizer;
|
||||
|
||||
signal dismiss();
|
||||
signal discard();
|
||||
signal discarded();
|
||||
|
||||
function handleDismiss() {}
|
||||
function handleDiscard() {}
|
||||
|
||||
onVisualizerChanged: {
|
||||
if (!visualizer) {
|
||||
expireAnim.stop();
|
||||
timePercentage = 1;
|
||||
}
|
||||
|
||||
if (!visualizer && destroyOnInvisible) this.destroy();
|
||||
}
|
||||
|
||||
function untrack() {
|
||||
destroyOnInvisible = true;
|
||||
if (!visualizer) this.destroy();
|
||||
}
|
||||
|
||||
property int expireTimeout: -1
|
||||
property real timePercentage: 1
|
||||
property int pauseCounter: 0
|
||||
readonly property bool shouldPause: root.pauseCounter != 0 || (NotificationManager.lastHoveredNotif?.pauseCounter ?? 0) != 0
|
||||
|
||||
onPauseCounterChanged: {
|
||||
if (pauseCounter > 0) {
|
||||
NotificationManager.lastHoveredNotif = this;
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation on timePercentage {
|
||||
id: expireAnim
|
||||
running: expireTimeout != 0
|
||||
paused: running && root.shouldPause && to == 0
|
||||
duration: expireTimeout == -1 ? 10000 : expireTimeout
|
||||
to: 0
|
||||
onFinished: {
|
||||
if (!inTray) root.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
onInTrayChanged: {
|
||||
if (inTray) {
|
||||
expireAnim.stop();
|
||||
expireAnim.duration = 300 * (1 - timePercentage);
|
||||
expireAnim.to = 1;
|
||||
expireAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
95
modules/user/modules/quickshell/shell/notifications/test.qml
Normal file
|
@ -0,0 +1,95 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../components"
|
||||
|
||||
ShellRoot {
|
||||
Component {
|
||||
id: demoNotif
|
||||
|
||||
FlickableNotification {
|
||||
contentItem: Rectangle {
|
||||
color: "white"
|
||||
border.color: "blue"
|
||||
border.width: 2
|
||||
radius: 10
|
||||
width: 400
|
||||
height: 150
|
||||
}
|
||||
|
||||
onLeftViewBounds: this.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
property Component testComponent: TrackedNotification {
|
||||
id: notification
|
||||
|
||||
renderComponent: Rectangle {
|
||||
color: "white"
|
||||
border.color: "blue"
|
||||
border.width: 2
|
||||
radius: 10
|
||||
width: 400
|
||||
height: 150
|
||||
|
||||
ColumnLayout {
|
||||
Button {
|
||||
text: "dismiss"
|
||||
onClicked: notification.dismiss();
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "discard"
|
||||
onClicked: notification.discard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDismiss() {
|
||||
console.log(`dismiss (sub)`)
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
console.log(`discard (sub)`)
|
||||
}
|
||||
|
||||
Component.onDestruction: console.log(`destroy (sub)`)
|
||||
};
|
||||
|
||||
property Component realComponent: DaemonNotification {
|
||||
id: dn
|
||||
}
|
||||
|
||||
Daemon {
|
||||
onNotification: notification => {
|
||||
notification.tracked = true;
|
||||
|
||||
const o = realComponent.createObject(null, { notif: notification });
|
||||
display.addNotification(o);
|
||||
}
|
||||
}
|
||||
|
||||
FloatingWindow {
|
||||
color: "transparent"
|
||||
|
||||
ColumnLayout {
|
||||
x: 5
|
||||
|
||||
Button {
|
||||
visible: false
|
||||
text: "add notif"
|
||||
|
||||
onClicked: {
|
||||
//const notif = demoNotif.createObject(stack);
|
||||
//stack.children = [...stack.children, notif];
|
||||
const notif = testComponent.createObject(null);
|
||||
display.addNotification(notif);
|
||||
}
|
||||
}
|
||||
|
||||
//ZHVStack { id: stack }
|
||||
NotificationDisplay { id: display }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// very bad code DO NOT COPY
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
@ -25,7 +26,7 @@ Scope {
|
|||
|
||||
Process {
|
||||
id: grimProc
|
||||
command: ["grim", "-l", "0", path]
|
||||
command: ["grim", "-l", "0", root.path]
|
||||
onExited: code => {
|
||||
if (code == 0) {
|
||||
root.visible = true
|
||||
|
@ -40,12 +41,11 @@ Scope {
|
|||
id: magickProc
|
||||
command: [
|
||||
"magick",
|
||||
path,
|
||||
"-crop",
|
||||
`${selection.w}x${selection.h}+${selection.x}+${selection.y}`,
|
||||
"-quality",
|
||||
"70",
|
||||
path,
|
||||
root.path,
|
||||
"-crop", `${selection.w}x${selection.h}+${selection.x}+${selection.y}`,
|
||||
"-quality", "70",
|
||||
"-page", "0x0+0+0", // removes page size and shot position
|
||||
root.path,
|
||||
]
|
||||
|
||||
onExited: wlCopy.running = true;
|
||||
|
@ -53,14 +53,14 @@ Scope {
|
|||
|
||||
Process {
|
||||
id: wlCopy
|
||||
command: ["sh", "-c", `wl-copy < '${path}'`]
|
||||
command: ["sh", "-c", `wl-copy < '${root.path}'`]
|
||||
|
||||
onExited: shootingComplete = true;
|
||||
onExited: root.shootingComplete = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cleanupProc
|
||||
command: ["rm", path]
|
||||
command: ["rm", root.path]
|
||||
}
|
||||
|
||||
QtObject {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
PanelWindow {
|
||||
visible: false
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
right: true
|
||||
top: true
|
||||
bottom: true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import QtQuick
|
||||
|
||||
ShaderEffect {
|
||||
property Item overlayItem;
|
||||
property point overlayPos: Qt.point(overlayItem.x, overlayItem.y);
|
||||
|
||||
fragmentShader: Qt.resolvedUrl("masked_overlay.frag.qsb")
|
||||
|
||||
property point pOverlayPos: Qt.point(
|
||||
overlayPos.x / width,
|
||||
overlayPos.y / height
|
||||
);
|
||||
|
||||
property point pOverlaySize: Qt.point(
|
||||
overlayItem.width / width,
|
||||
overlayItem.height / height
|
||||
);
|
||||
|
||||
property point pMergeInset: Qt.point(
|
||||
3 / width,
|
||||
3 / height
|
||||
);
|
||||
|
||||
property real pMergeCutoff: 0.15
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
#version 440
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 1) out vec4 fragColor;
|
||||
layout(binding = 1) uniform sampler2D source;
|
||||
layout(binding = 2) uniform sampler2D overlayItem;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
vec2 pOverlayPos;
|
||||
vec2 pOverlaySize;
|
||||
vec2 pMergeInset;
|
||||
float pMergeCutoff;
|
||||
};
|
||||
|
||||
void main() {
|
||||
vec2 overlayCoord = (qt_TexCoord0 - pOverlayPos) / pOverlaySize;
|
||||
|
||||
if (overlayCoord.x >= 0 && overlayCoord.y >= 0 && overlayCoord.x < 1 && overlayCoord.y < 1) {
|
||||
fragColor = texture(overlayItem, overlayCoord);
|
||||
|
||||
if (fragColor.a != 0) {
|
||||
vec4 baseColor = texture(source, qt_TexCoord0);
|
||||
// imperfect but visually good enough for now. if more is needed we'll probably need a mask tex
|
||||
if (baseColor.a != 0
|
||||
&& fragColor.a < pMergeCutoff
|
||||
&& (texture(overlayItem, overlayCoord + vec2(0, pMergeInset.y)).a == 0
|
||||
|| texture(overlayItem, overlayCoord + vec2(pMergeInset.x, 0)).a == 0
|
||||
|| texture(overlayItem, overlayCoord + vec2(0, -pMergeInset.y)).a == 0
|
||||
|| texture(overlayItem, overlayCoord + vec2(-pMergeInset.x, 0)).a == 0)) {
|
||||
fragColor += baseColor * (1 - fragColor.a);
|
||||
}
|
||||
|
||||
fragColor *= qt_Opacity;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fragColor = texture(source, qt_TexCoord0) * qt_Opacity;
|
||||
}
|