mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
quickshell and hyprland additions
This commit is contained in:
205
.config/quickshell/caelestia/modules/bar/Bar.qml
Normal file
205
.config/quickshell/caelestia/modules/bar/Bar.qml
Normal file
@@ -0,0 +1,205 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.config
|
||||
import "popouts" as BarPopouts
|
||||
import "components"
|
||||
import "components/workspaces"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property PersistentProperties visibilities
|
||||
required property BarPopouts.Wrapper popouts
|
||||
readonly property int vPadding: Appearance.padding.large
|
||||
|
||||
function closeTray(): void {
|
||||
if (!Config.bar.tray.compact)
|
||||
return;
|
||||
|
||||
for (let i = 0; i < repeater.count; i++) {
|
||||
const item = repeater.itemAt(i);
|
||||
if (item?.enabled && item.id === "tray") {
|
||||
item.item.expanded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkPopout(y: real): void {
|
||||
const ch = childAt(width / 2, y) as WrappedLoader;
|
||||
|
||||
if (ch?.id !== "tray")
|
||||
closeTray();
|
||||
|
||||
if (!ch) {
|
||||
popouts.hasCurrent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = ch.id;
|
||||
const top = ch.y;
|
||||
const item = ch.item;
|
||||
const itemHeight = item.implicitHeight;
|
||||
|
||||
if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
|
||||
const items = item.items;
|
||||
const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y);
|
||||
if (icon) {
|
||||
popouts.currentName = icon.name;
|
||||
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, 0, icon.implicitHeight / 2).y);
|
||||
popouts.hasCurrent = true;
|
||||
}
|
||||
} else if (id === "tray" && Config.bar.popouts.tray) {
|
||||
if (!Config.bar.tray.compact || (item.expanded && !item.expandIcon.contains(mapToItem(item.expandIcon, item.implicitWidth / 2, y)))) {
|
||||
const index = Math.floor(((y - top - item.padding * 2 + item.spacing) / item.layout.implicitHeight) * item.items.count);
|
||||
const trayItem = item.items.itemAt(index);
|
||||
if (trayItem) {
|
||||
popouts.currentName = `traymenu${index}`;
|
||||
popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y);
|
||||
popouts.hasCurrent = true;
|
||||
} else {
|
||||
popouts.hasCurrent = false;
|
||||
}
|
||||
} else {
|
||||
popouts.hasCurrent = false;
|
||||
item.expanded = true;
|
||||
}
|
||||
} else if (id === "activeWindow" && Config.bar.popouts.activeWindow) {
|
||||
popouts.currentName = id.toLowerCase();
|
||||
popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y;
|
||||
popouts.hasCurrent = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleWheel(y: real, angleDelta: point): void {
|
||||
const ch = childAt(width / 2, y) as WrappedLoader;
|
||||
if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) {
|
||||
// Workspace scroll
|
||||
const mon = (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor);
|
||||
const specialWs = mon?.lastIpcObject.specialWorkspace.name;
|
||||
if (specialWs?.length > 0)
|
||||
Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`);
|
||||
else if (angleDelta.y < 0 || (Config.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1)
|
||||
Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`);
|
||||
} else if (y < screen.height / 2 && Config.bar.scrollActions.volume) {
|
||||
// Volume scroll on top half
|
||||
if (angleDelta.y > 0)
|
||||
Audio.incrementVolume();
|
||||
else if (angleDelta.y < 0)
|
||||
Audio.decrementVolume();
|
||||
} else if (Config.bar.scrollActions.brightness) {
|
||||
// Brightness scroll on bottom half
|
||||
const monitor = Brightness.getMonitorForScreen(screen);
|
||||
if (angleDelta.y > 0)
|
||||
monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement);
|
||||
else if (angleDelta.y < 0)
|
||||
monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement);
|
||||
}
|
||||
}
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: Config.bar.entries
|
||||
|
||||
DelegateChooser {
|
||||
role: "id"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "spacer"
|
||||
delegate: WrappedLoader {
|
||||
Layout.fillHeight: enabled
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "logo"
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: OsIcon {}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "workspaces"
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: Workspaces {
|
||||
screen: root.screen
|
||||
}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "activeWindow"
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: ActiveWindow {
|
||||
bar: root
|
||||
monitor: Brightness.getMonitorForScreen(root.screen)
|
||||
}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "tray"
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: Tray {}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "clock"
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: Clock {}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "statusIcons"
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: StatusIcons {}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "power"
|
||||
delegate: WrappedLoader {
|
||||
sourceComponent: Power {
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component WrappedLoader: Loader {
|
||||
required property bool enabled
|
||||
required property string id
|
||||
required property int index
|
||||
|
||||
function findFirstEnabled(): Item {
|
||||
const count = repeater.count;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const item = repeater.itemAt(i);
|
||||
if (item?.enabled)
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLastEnabled(): Item {
|
||||
for (let i = repeater.count - 1; i >= 0; i--) {
|
||||
const item = repeater.itemAt(i);
|
||||
if (item?.enabled)
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
// Cursed ahh thing to add padding to first and last enabled components
|
||||
Layout.topMargin: findFirstEnabled() === this ? root.vPadding : 0
|
||||
Layout.bottomMargin: findLastEnabled() === this ? root.vPadding : 0
|
||||
|
||||
visible: enabled
|
||||
active: enabled
|
||||
}
|
||||
}
|
||||
87
.config/quickshell/caelestia/modules/bar/BarWrapper.qml
Normal file
87
.config/quickshell/caelestia/modules/bar/BarWrapper.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.config
|
||||
import "popouts" as BarPopouts
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property PersistentProperties visibilities
|
||||
required property BarPopouts.Wrapper popouts
|
||||
required property bool disabled
|
||||
|
||||
readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness)
|
||||
readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2
|
||||
readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness
|
||||
readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered)
|
||||
property bool isHovered
|
||||
|
||||
function closeTray(): void {
|
||||
content.item?.closeTray();
|
||||
}
|
||||
|
||||
function checkPopout(y: real): void {
|
||||
content.item?.checkPopout(y);
|
||||
}
|
||||
|
||||
function handleWheel(y: real, angleDelta: point): void {
|
||||
content.item?.handleWheel(y, angleDelta);
|
||||
}
|
||||
|
||||
visible: width > Config.border.thickness
|
||||
implicitWidth: Config.border.thickness
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.shouldBeVisible
|
||||
|
||||
PropertyChanges {
|
||||
root.implicitWidth: root.contentWidth
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "visible"
|
||||
|
||||
Anim {
|
||||
target: root
|
||||
property: "implicitWidth"
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "visible"
|
||||
to: ""
|
||||
|
||||
Anim {
|
||||
target: root
|
||||
property: "implicitWidth"
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Loader {
|
||||
id: content
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
active: root.shouldBeVisible || root.visible
|
||||
|
||||
sourceComponent: Bar {
|
||||
width: root.contentWidth
|
||||
screen: root.screen
|
||||
visibilities: root.visibilities
|
||||
popouts: root.popouts
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var bar
|
||||
required property Brightness.Monitor monitor
|
||||
property color colour: Colours.palette.m3primary
|
||||
|
||||
readonly property int maxHeight: {
|
||||
const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer");
|
||||
const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0);
|
||||
// Length - 2 cause repeater counts as a child
|
||||
return bar.height - otherHeight - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2;
|
||||
}
|
||||
property Title current: text1
|
||||
|
||||
clip: true
|
||||
implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight)
|
||||
implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
animate: true
|
||||
text: Icons.getAppCategoryIcon(Hypr.activeToplevel?.lastIpcObject.class, "desktop_windows")
|
||||
color: root.colour
|
||||
}
|
||||
|
||||
Title {
|
||||
id: text1
|
||||
}
|
||||
|
||||
Title {
|
||||
id: text2
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
text: Hypr.activeToplevel?.title ?? qsTr("Desktop")
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
font.family: Appearance.font.family.mono
|
||||
elide: Qt.ElideRight
|
||||
elideWidth: root.maxHeight - icon.height
|
||||
|
||||
onTextChanged: {
|
||||
const next = root.current === text1 ? text2 : text1;
|
||||
next.text = elidedText;
|
||||
root.current = next;
|
||||
}
|
||||
onElideWidthChanged: root.current.text = elidedText
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
component Title: StyledText {
|
||||
id: text
|
||||
|
||||
anchors.horizontalCenter: icon.horizontalCenter
|
||||
anchors.top: icon.bottom
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
|
||||
font.pointSize: metrics.font.pointSize
|
||||
font.family: metrics.font.family
|
||||
color: root.colour
|
||||
opacity: root.current === this ? 1 : 0
|
||||
|
||||
transform: [
|
||||
Translate {
|
||||
x: Config.bar.activeWindow.inverted ? -implicitWidth + text.implicitHeight : 0
|
||||
},
|
||||
Rotation {
|
||||
angle: Config.bar.activeWindow.inverted ? 270 : 90
|
||||
origin.x: text.implicitHeight / 2
|
||||
origin.y: text.implicitHeight / 2
|
||||
}
|
||||
]
|
||||
|
||||
width: implicitHeight
|
||||
height: implicitWidth
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property color colour: Colours.palette.m3tertiary
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Loader {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
active: Config.bar.clock.showIcon
|
||||
visible: active
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "calendar_month"
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: text
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
horizontalAlignment: StyledText.AlignHCenter
|
||||
text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm")
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
font.family: Appearance.font.family.mono
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import QtQuick
|
||||
import qs.components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: Appearance.font.size.large * 1.2
|
||||
implicitHeight: Appearance.font.size.large * 1.2
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const visibilities = Visibilities.getForActive();
|
||||
visibilities.launcher = !visibilities.launcher;
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon
|
||||
}
|
||||
|
||||
Component {
|
||||
id: caelestiaLogo
|
||||
|
||||
Logo {
|
||||
implicitWidth: Appearance.font.size.large * 1.8
|
||||
implicitHeight: Appearance.font.size.large * 1.8
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: distroIcon
|
||||
|
||||
ColouredIcon {
|
||||
source: SysInfo.osLogo
|
||||
implicitSize: Appearance.font.size.large * 1.2
|
||||
colour: Colours.palette.m3tertiary
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: icon.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
// Cursed workaround to make the height larger than the parent
|
||||
anchors.fill: undefined
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
root.visibilities.session = !root.visibilities.session;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -1
|
||||
|
||||
text: "power_settings_new"
|
||||
color: Colours.palette.m3error
|
||||
font.bold: true
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import qs.components
|
||||
import qs.modules.controlcenter
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: icon.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
// Cursed workaround to make the height larger than the parent
|
||||
anchors.fill: undefined
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
WindowFactory.create(null, {
|
||||
active: "network"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -1
|
||||
|
||||
text: "settings"
|
||||
color: Colours.palette.m3onSurface
|
||||
font.bold: true
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import qs.components
|
||||
import qs.modules.controlcenter
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: icon.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
// Cursed workaround to make the height larger than the parent
|
||||
anchors.fill: undefined
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
WindowFactory.create(null, {
|
||||
active: "network"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -1
|
||||
|
||||
text: "settings"
|
||||
color: Colours.palette.m3onSurface
|
||||
font.bold: true
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Services.UPower
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
property color colour: Colours.palette.m3secondary
|
||||
readonly property alias items: iconColumn
|
||||
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
clip: true
|
||||
implicitWidth: Config.bar.sizes.innerWidth
|
||||
implicitHeight: iconColumn.implicitHeight + Appearance.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0)
|
||||
|
||||
ColumnLayout {
|
||||
id: iconColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
// Lock keys status
|
||||
WrappedLoader {
|
||||
name: "lockstatus"
|
||||
active: Config.bar.status.showLockStatus
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
implicitWidth: capslockIcon.implicitWidth
|
||||
implicitHeight: Hypr.capsLock ? capslockIcon.implicitHeight : 0
|
||||
|
||||
MaterialIcon {
|
||||
id: capslockIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
scale: Hypr.capsLock ? 1 : 0.5
|
||||
opacity: Hypr.capsLock ? 1 : 0
|
||||
|
||||
text: "keyboard_capslock_badge"
|
||||
color: root.colour
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.topMargin: Hypr.capsLock && Hypr.numLock ? iconColumn.spacing : 0
|
||||
|
||||
implicitWidth: numlockIcon.implicitWidth
|
||||
implicitHeight: Hypr.numLock ? numlockIcon.implicitHeight : 0
|
||||
|
||||
MaterialIcon {
|
||||
id: numlockIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
scale: Hypr.numLock ? 1 : 0.5
|
||||
opacity: Hypr.numLock ? 1 : 0
|
||||
|
||||
text: "looks_one"
|
||||
color: root.colour
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audio icon
|
||||
WrappedLoader {
|
||||
name: "audio"
|
||||
active: Config.bar.status.showAudio
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
animate: true
|
||||
text: Icons.getVolumeIcon(Audio.volume, Audio.muted)
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
|
||||
// Microphone icon
|
||||
WrappedLoader {
|
||||
name: "audio"
|
||||
active: Config.bar.status.showMicrophone
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
animate: true
|
||||
text: Icons.getMicVolumeIcon(Audio.sourceVolume, Audio.sourceMuted)
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard layout icon
|
||||
WrappedLoader {
|
||||
name: "kblayout"
|
||||
active: Config.bar.status.showKbLayout
|
||||
|
||||
sourceComponent: StyledText {
|
||||
animate: true
|
||||
text: Hypr.kbLayout
|
||||
color: root.colour
|
||||
font.family: Appearance.font.family.mono
|
||||
}
|
||||
}
|
||||
|
||||
// Network icon
|
||||
WrappedLoader {
|
||||
name: "network"
|
||||
active: Config.bar.status.showNetwork && (!Nmcli.activeEthernet || Config.bar.status.showWifi)
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
animate: true
|
||||
text: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
|
||||
// Ethernet icon
|
||||
WrappedLoader {
|
||||
name: "ethernet"
|
||||
active: Config.bar.status.showNetwork && Nmcli.activeEthernet
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
animate: true
|
||||
text: "cable"
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
|
||||
// Bluetooth section
|
||||
WrappedLoader {
|
||||
Layout.preferredHeight: implicitHeight
|
||||
|
||||
name: "bluetooth"
|
||||
active: Config.bar.status.showBluetooth
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
// Bluetooth icon
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
text: {
|
||||
if (!Bluetooth.defaultAdapter?.enabled)
|
||||
return "bluetooth_disabled";
|
||||
if (Bluetooth.devices.values.some(d => d.connected))
|
||||
return "bluetooth_connected";
|
||||
return "bluetooth";
|
||||
}
|
||||
color: root.colour
|
||||
}
|
||||
|
||||
// Connected bluetooth devices
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: device
|
||||
|
||||
required property BluetoothDevice modelData
|
||||
|
||||
animate: true
|
||||
text: Icons.getBluetoothIcon(modelData?.icon)
|
||||
color: root.colour
|
||||
fill: 1
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: device.modelData?.state !== BluetoothDeviceState.Connected
|
||||
alwaysRunToEnd: true
|
||||
loops: Animation.Infinite
|
||||
|
||||
Anim {
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
Anim {
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
// Battery icon
|
||||
WrappedLoader {
|
||||
name: "battery"
|
||||
active: Config.bar.status.showBattery
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
animate: true
|
||||
text: {
|
||||
if (!UPower.displayDevice.isLaptopBattery) {
|
||||
if (PowerProfiles.profile === PowerProfile.PowerSaver)
|
||||
return "energy_savings_leaf";
|
||||
if (PowerProfiles.profile === PowerProfile.Performance)
|
||||
return "rocket_launch";
|
||||
return "balance";
|
||||
}
|
||||
|
||||
const perc = UPower.displayDevice.percentage;
|
||||
const charging = [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state);
|
||||
if (perc === 1)
|
||||
return charging ? "battery_charging_full" : "battery_full";
|
||||
let level = Math.floor(perc * 7);
|
||||
if (charging && (level === 4 || level === 1))
|
||||
level--;
|
||||
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
|
||||
}
|
||||
color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error
|
||||
fill: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component WrappedLoader: Loader {
|
||||
required property string name
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: active
|
||||
}
|
||||
}
|
||||
121
.config/quickshell/caelestia/modules/bar/components/Tray.qml
Normal file
121
.config/quickshell/caelestia/modules/bar/components/Tray.qml
Normal file
@@ -0,0 +1,121 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
readonly property alias layout: layout
|
||||
readonly property alias items: items
|
||||
readonly property alias expandIcon: expandIcon
|
||||
|
||||
readonly property int padding: Config.bar.tray.background ? Appearance.padding.normal : Appearance.padding.small
|
||||
readonly property int spacing: Config.bar.tray.background ? Appearance.spacing.small : 0
|
||||
|
||||
property bool expanded
|
||||
|
||||
readonly property real nonAnimHeight: {
|
||||
if (!Config.bar.tray.compact)
|
||||
return layout.implicitHeight + padding * 2;
|
||||
return (expanded ? expandIcon.implicitHeight + layout.implicitHeight + spacing : expandIcon.implicitHeight) + padding * 2;
|
||||
}
|
||||
|
||||
clip: true
|
||||
visible: height > 0
|
||||
|
||||
implicitWidth: Config.bar.sizes.innerWidth
|
||||
implicitHeight: nonAnimHeight
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
Column {
|
||||
id: layout
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: root.padding
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
from: 0
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: items
|
||||
|
||||
model: ScriptModel {
|
||||
values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id))
|
||||
}
|
||||
|
||||
TrayItem {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: expandIcon
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
active: Config.bar.tray.compact && items.count > 0
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitWidth: expandIconInner.implicitWidth
|
||||
implicitHeight: expandIconInner.implicitHeight - Appearance.padding.small * 2
|
||||
|
||||
MaterialIcon {
|
||||
id: expandIconInner
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Config.bar.tray.background ? Appearance.padding.small : -Appearance.padding.small
|
||||
text: "expand_less"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
rotation: root.expanded ? 180 : 0
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on anchors.bottomMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
required property SystemTrayItem modelData
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
implicitWidth: Appearance.font.size.small * 2
|
||||
implicitHeight: Appearance.font.size.small * 2
|
||||
|
||||
onClicked: event => {
|
||||
if (event.button === Qt.LeftButton)
|
||||
modelData.activate();
|
||||
else
|
||||
modelData.secondaryActivate();
|
||||
}
|
||||
|
||||
ColouredIcon {
|
||||
id: icon
|
||||
|
||||
anchors.fill: parent
|
||||
source: Icons.getTrayIcon(root.modelData.id, root.modelData.icon)
|
||||
colour: Colours.palette.m3secondary
|
||||
layer.enabled: Config.bar.tray.recolour
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
required property int activeWsId
|
||||
required property Repeater workspaces
|
||||
required property Item mask
|
||||
|
||||
readonly property int currentWsIdx: {
|
||||
let i = activeWsId - 1;
|
||||
while (i < 0)
|
||||
i += Config.bar.workspaces.shown;
|
||||
return i % Config.bar.workspaces.shown;
|
||||
}
|
||||
|
||||
property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0
|
||||
property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0
|
||||
property real currentSize: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.size ?? 0 : 0
|
||||
property real offset: Math.min(leading, trailing)
|
||||
property real size: {
|
||||
const s = Math.abs(leading - trailing) + currentSize;
|
||||
if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) {
|
||||
const ws = workspaces.itemAt(lastWs);
|
||||
// console.log(ws, lastWs);
|
||||
return ws ? Math.min(ws.y + ws.size - offset, s) : 0;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
property int cWs
|
||||
property int lastWs
|
||||
|
||||
onCurrentWsIdxChanged: {
|
||||
lastWs = cWs;
|
||||
cWs = currentWsIdx;
|
||||
}
|
||||
|
||||
clip: true
|
||||
y: offset + mask.y
|
||||
implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2
|
||||
implicitHeight: size
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primary
|
||||
|
||||
Colouriser {
|
||||
source: root.mask
|
||||
sourceColor: Colours.palette.m3onSurface
|
||||
colorizationColor: Colours.palette.m3onPrimary
|
||||
|
||||
x: 0
|
||||
y: -parent.offset
|
||||
implicitWidth: root.mask.implicitWidth
|
||||
implicitHeight: root.mask.implicitHeight
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Behavior on leading {
|
||||
enabled: Config.bar.workspaces.activeTrail
|
||||
|
||||
EAnim {}
|
||||
}
|
||||
|
||||
Behavior on trailing {
|
||||
enabled: Config.bar.workspaces.activeTrail
|
||||
|
||||
EAnim {
|
||||
duration: Appearance.anim.durations.normal * 2
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on currentSize {
|
||||
enabled: Config.bar.workspaces.activeTrail
|
||||
|
||||
EAnim {}
|
||||
}
|
||||
|
||||
Behavior on offset {
|
||||
enabled: !Config.bar.workspaces.activeTrail
|
||||
|
||||
EAnim {}
|
||||
}
|
||||
|
||||
Behavior on size {
|
||||
enabled: !Config.bar.workspaces.activeTrail
|
||||
|
||||
EAnim {}
|
||||
}
|
||||
|
||||
component EAnim: Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Repeater workspaces
|
||||
required property var occupied
|
||||
required property int groupOffset
|
||||
|
||||
property list<var> pills: []
|
||||
|
||||
onOccupiedChanged: {
|
||||
if (!occupied)
|
||||
return;
|
||||
let count = 0;
|
||||
const start = groupOffset;
|
||||
const end = start + Config.bar.workspaces.shown;
|
||||
for (const [ws, occ] of Object.entries(occupied)) {
|
||||
if (ws > start && ws <= end && occ) {
|
||||
const isFirstInGroup = Number(ws) === start + 1;
|
||||
const isLastInGroup = Number(ws) === end;
|
||||
if (isFirstInGroup || !occupied[ws - 1]) {
|
||||
if (pills[count])
|
||||
pills[count].start = ws;
|
||||
else
|
||||
pills.push(pillComp.createObject(root, {
|
||||
start: ws
|
||||
}));
|
||||
count++;
|
||||
}
|
||||
if ((isLastInGroup || !occupied[ws + 1]) && pills[count - 1])
|
||||
pills[count - 1].end = ws;
|
||||
}
|
||||
}
|
||||
if (pills.length > count)
|
||||
pills.splice(count, pills.length - count).forEach(p => p.destroy());
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.pills.filter(p => p)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: rect
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null
|
||||
readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null
|
||||
|
||||
function getWsIdx(ws: int): int {
|
||||
let i = ws - 1;
|
||||
while (i < 0)
|
||||
i += Config.bar.workspaces.shown;
|
||||
return i % Config.bar.workspaces.shown;
|
||||
}
|
||||
|
||||
anchors.horizontalCenter: root.horizontalCenter
|
||||
|
||||
y: (start?.y ?? 0) - 1
|
||||
implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + 2
|
||||
implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0
|
||||
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
scale: 0
|
||||
Component.onCompleted: scale = 1
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Pill: QtObject {
|
||||
property int start
|
||||
property int end
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pillComp
|
||||
|
||||
Pill {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
|
||||
readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name ?? ""
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: mask
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.3
|
||||
color: Qt.rgba(0, 0, 0, 1)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.7
|
||||
color: Qt.rgba(0, 0, 0, 1)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
implicitHeight: parent.height / 2
|
||||
opacity: view.contentY > 0 ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
implicitHeight: parent.height / 2
|
||||
opacity: view.contentY < view.contentHeight - parent.height + Appearance.padding.small ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: view
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
interactive: false
|
||||
|
||||
currentIndex: model.values.findIndex(w => w.name === root.activeSpecial)
|
||||
onCurrentIndexChanged: currentIndex = Qt.binding(() => model.values.findIndex(w => w.name === root.activeSpecial))
|
||||
|
||||
model: ScriptModel {
|
||||
values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!Config.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor))
|
||||
}
|
||||
|
||||
preferredHighlightBegin: 0
|
||||
preferredHighlightEnd: height
|
||||
highlightRangeMode: ListView.StrictlyEnforceRange
|
||||
|
||||
highlightFollowsCurrentItem: false
|
||||
highlight: Item {
|
||||
y: view.currentItem?.y ?? 0
|
||||
implicitHeight: view.currentItem?.size ?? 0
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: ws
|
||||
|
||||
required property HyprlandWorkspace modelData
|
||||
readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0)
|
||||
property int wsId
|
||||
property string icon
|
||||
property bool hasWindows
|
||||
|
||||
anchors.left: view.contentItem.left
|
||||
anchors.right: view.contentItem.right
|
||||
|
||||
spacing: 0
|
||||
|
||||
Component.onCompleted: {
|
||||
wsId = modelData.id;
|
||||
icon = Icons.getSpecialWsIcon(modelData.name);
|
||||
hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0;
|
||||
}
|
||||
|
||||
// Hacky thing cause modelData gets destroyed before the remove anim finishes
|
||||
Connections {
|
||||
target: ws.modelData
|
||||
|
||||
function onIdChanged(): void {
|
||||
if (ws.modelData)
|
||||
ws.wsId = ws.modelData.id;
|
||||
}
|
||||
|
||||
function onNameChanged(): void {
|
||||
if (ws.modelData)
|
||||
ws.icon = Icons.getSpecialWsIcon(ws.modelData.name);
|
||||
}
|
||||
|
||||
function onLastIpcObjectChanged(): void {
|
||||
if (ws.modelData)
|
||||
ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Config.bar.workspaces
|
||||
|
||||
function onShowWindowsOnSpecialWorkspacesChanged(): void {
|
||||
if (ws.modelData)
|
||||
ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0;
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: label
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2
|
||||
|
||||
sourceComponent: ws.icon.length === 1 ? letterComp : iconComp
|
||||
|
||||
Component {
|
||||
id: iconComp
|
||||
|
||||
MaterialIcon {
|
||||
fill: 1
|
||||
text: ws.icon
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: letterComp
|
||||
|
||||
StyledText {
|
||||
text: ws.icon
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: windows
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
|
||||
visible: active
|
||||
active: ws.hasWindows
|
||||
|
||||
sourceComponent: Column {
|
||||
spacing: 0
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
from: 0
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
required property var modelData
|
||||
|
||||
grade: 0
|
||||
text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal")
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
from: 0
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "scale"
|
||||
to: 0.5
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
}
|
||||
}
|
||||
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.bar.workspaces.activeIndicator
|
||||
anchors.fill: parent
|
||||
|
||||
sourceComponent: Item {
|
||||
StyledClippingRect {
|
||||
id: indicator
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
y: (view.currentItem?.y ?? 0) - view.contentY
|
||||
implicitHeight: view.currentItem?.size ?? 0
|
||||
|
||||
color: Colours.palette.m3tertiary
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
Colouriser {
|
||||
source: view
|
||||
sourceColor: Colours.palette.m3onSurface
|
||||
colorizationColor: Colours.palette.m3onTertiary
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
x: 0
|
||||
y: -indicator.y
|
||||
implicitWidth: view.width
|
||||
implicitHeight: view.height
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
property real startY
|
||||
|
||||
anchors.fill: view
|
||||
|
||||
drag.target: view.contentItem
|
||||
drag.axis: Drag.YAxis
|
||||
drag.maximumY: 0
|
||||
drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small)
|
||||
|
||||
onPressed: event => startY = event.y
|
||||
|
||||
onClicked: event => {
|
||||
if (Math.abs(event.y - startY) > drag.threshold)
|
||||
return;
|
||||
|
||||
const ws = view.itemAt(event.x, event.y);
|
||||
if (ws?.modelData)
|
||||
Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`);
|
||||
else
|
||||
Hypr.dispatch("togglespecialworkspace special");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property int index
|
||||
required property int activeWsId
|
||||
required property var occupied
|
||||
required property int groupOffset
|
||||
|
||||
readonly property bool isWorkspace: true // Flag for finding workspace children
|
||||
// Unanimated prop for others to use as reference
|
||||
readonly property int size: implicitHeight + (hasWindows ? Appearance.padding.small : 0)
|
||||
|
||||
readonly property int ws: groupOffset + index + 1
|
||||
readonly property bool isOccupied: occupied[ws] ?? false
|
||||
readonly property bool hasWindows: isOccupied && Config.bar.workspaces.showWindows
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredHeight: size
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
id: indicator
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2
|
||||
|
||||
animate: true
|
||||
text: {
|
||||
const ws = Hypr.workspaces.values.find(w => w.id === root.ws);
|
||||
const wsName = !ws || ws.name == root.ws ? root.ws : ws.name[0];
|
||||
let displayName = wsName.toString();
|
||||
if (Config.bar.workspaces.capitalisation.toLowerCase() === "upper") {
|
||||
displayName = displayName.toUpperCase();
|
||||
} else if (Config.bar.workspaces.capitalisation.toLowerCase() === "lower") {
|
||||
displayName = displayName.toLowerCase();
|
||||
}
|
||||
const label = Config.bar.workspaces.label || displayName;
|
||||
const occupiedLabel = Config.bar.workspaces.occupiedLabel || label;
|
||||
const activeLabel = Config.bar.workspaces.activeLabel || (root.isOccupied ? occupiedLabel : label);
|
||||
return root.activeWsId === root.ws ? activeLabel : root.isOccupied ? occupiedLabel : label;
|
||||
}
|
||||
color: Config.bar.workspaces.occupiedBg || root.isOccupied || root.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.layer(Colours.palette.m3outlineVariant, 2)
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: windows
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: -Config.bar.sizes.innerWidth / 10
|
||||
|
||||
visible: active
|
||||
active: root.hasWindows
|
||||
|
||||
sourceComponent: Column {
|
||||
spacing: 0
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
from: 0
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
Anim {
|
||||
properties: "x,y"
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
required property var modelData
|
||||
|
||||
grade: 0
|
||||
text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal")
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.components
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
||||
StyledClippingRect {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name !== ""
|
||||
readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId
|
||||
|
||||
readonly property var occupied: Hypr.workspaces.values.reduce((acc, curr) => {
|
||||
acc[curr.id] = curr.lastIpcObject.windows > 0;
|
||||
return acc;
|
||||
}, {})
|
||||
readonly property int groupOffset: Math.floor((activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown
|
||||
|
||||
property real blur: onSpecial ? 1 : 0
|
||||
|
||||
implicitWidth: Config.bar.sizes.innerWidth
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
scale: root.onSpecial ? 0.8 : 1
|
||||
opacity: root.onSpecial ? 0.5 : 1
|
||||
|
||||
layer.enabled: root.blur > 0
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: root.blur
|
||||
blurMax: 32
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.bar.workspaces.occupiedBg
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.small
|
||||
|
||||
sourceComponent: OccupiedBg {
|
||||
workspaces: workspaces
|
||||
occupied: root.occupied
|
||||
groupOffset: root.groupOffset
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Math.floor(Appearance.spacing.small / 2)
|
||||
|
||||
Repeater {
|
||||
id: workspaces
|
||||
|
||||
model: Config.bar.workspaces.shown
|
||||
|
||||
Workspace {
|
||||
activeWsId: root.activeWsId
|
||||
occupied: root.occupied
|
||||
groupOffset: root.groupOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Config.bar.workspaces.activeIndicator
|
||||
|
||||
sourceComponent: ActiveIndicator {
|
||||
activeWsId: root.activeWsId
|
||||
workspaces: workspaces
|
||||
mask: layout
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: layout
|
||||
onClicked: event => {
|
||||
const ws = layout.childAt(event.x, event.y).ws;
|
||||
if (Hypr.activeWsId !== ws)
|
||||
Hypr.dispatch(`workspace ${ws}`);
|
||||
else
|
||||
Hypr.dispatch("togglespecialworkspace special");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: specialWs
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.small
|
||||
|
||||
active: opacity > 0
|
||||
|
||||
scale: root.onSpecial ? 1 : 0.5
|
||||
opacity: root.onSpecial ? 1 : 0
|
||||
|
||||
sourceComponent: SpecialWorkspaces {
|
||||
screen: root.screen
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on blur {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Item wrapper
|
||||
|
||||
implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2
|
||||
implicitHeight: child.implicitHeight
|
||||
|
||||
Column {
|
||||
id: child
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
id: detailsRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
IconImage {
|
||||
id: icon
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitSize: details.implicitHeight
|
||||
source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? "", "image-missing")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: details
|
||||
|
||||
spacing: 0
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: Hypr.activeToplevel?.title ?? ""
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: Hypr.activeToplevel?.lastIpcObject.class ?? ""
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitWidth: expandIcon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: expandIcon.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
function onClicked(): void {
|
||||
root.wrapper.detach("winfo");
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: expandIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: font.pointSize * 0.05
|
||||
|
||||
text: "chevron_right"
|
||||
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClippingWrapperRectangle {
|
||||
color: "transparent"
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
ScreencopyView {
|
||||
id: preview
|
||||
|
||||
captureSource: Hypr.activeToplevel?.wayland ?? null
|
||||
live: visible
|
||||
|
||||
constraintSize.width: Config.bar.sizes.windowPreviewSize
|
||||
constraintSize.height: Config.bar.sizes.windowPreviewSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
.config/quickshell/caelestia/modules/bar/popouts/Audio.qml
Normal file
120
.config/quickshell/caelestia/modules/bar/popouts/Audio.qml
Normal file
@@ -0,0 +1,120 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import "../../controlcenter/network"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var wrapper
|
||||
|
||||
implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
ButtonGroup {
|
||||
id: sinks
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
id: sources
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Output device")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Audio.sinks
|
||||
|
||||
StyledRadioButton {
|
||||
id: control
|
||||
|
||||
required property PwNode modelData
|
||||
|
||||
ButtonGroup.group: sinks
|
||||
checked: Audio.sink?.id === modelData.id
|
||||
onClicked: Audio.setAudioSink(modelData)
|
||||
text: modelData.description
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.smaller
|
||||
text: qsTr("Input device")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Audio.sources
|
||||
|
||||
StyledRadioButton {
|
||||
required property PwNode modelData
|
||||
|
||||
ButtonGroup.group: sources
|
||||
checked: Audio.source?.id === modelData.id
|
||||
onClicked: Audio.setAudioSource(modelData)
|
||||
text: modelData.description
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.smaller
|
||||
Layout.bottomMargin: -Appearance.spacing.small / 2
|
||||
text: qsTr("Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`)
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
CustomMouseArea {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
onWheel: event => {
|
||||
if (event.angleDelta.y > 0)
|
||||
Audio.incrementVolume();
|
||||
else if (event.angleDelta.y < 0)
|
||||
Audio.decrementVolume();
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: parent.implicitHeight
|
||||
|
||||
value: Audio.volume
|
||||
onMoved: Audio.setVolume(value)
|
||||
|
||||
Behavior on value {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconTextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
verticalPadding: Appearance.padding.small
|
||||
text: qsTr("Open settings")
|
||||
icon: "settings"
|
||||
|
||||
onClicked: root.wrapper.detach("audio")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
required property bool invertBottomRounding
|
||||
readonly property real rounding: wrapper.isDetached ? Appearance.rounding.normal : Config.border.rounding
|
||||
readonly property bool flatten: wrapper.width < rounding * 2
|
||||
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
|
||||
property real ibr: invertBottomRounding ? -1 : 1
|
||||
|
||||
property real sideRounding: startX > 0 ? -1 : 1
|
||||
|
||||
strokeWidth: -1
|
||||
fillColor: Colours.palette.m3surface
|
||||
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding * root.sideRounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: root.wrapper.width - root.roundingX * 2
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: root.wrapper.height - root.rounding * 2
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -root.roundingX * root.ibr
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr)
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -root.roundingX
|
||||
relativeY: root.rounding * root.sideRounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
Behavior on fillColor {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on ibr {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on sideRounding {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
230
.config/quickshell/caelestia/modules/bar/popouts/Battery.qml
Normal file
230
.config/quickshell/caelestia/modules/bar/popouts/Battery.qml
Normal file
@@ -0,0 +1,230 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell.Services.UPower
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
width: Config.bar.sizes.batteryWidth
|
||||
|
||||
StyledText {
|
||||
text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
function formatSeconds(s: int, fallback: string): string {
|
||||
const day = Math.floor(s / 86400);
|
||||
const hr = Math.floor(s / 3600) % 60;
|
||||
const min = Math.floor(s / 60) % 60;
|
||||
|
||||
let comps = [];
|
||||
if (day > 0)
|
||||
comps.push(`${day} days`);
|
||||
if (hr > 0)
|
||||
comps.push(`${hr} hours`);
|
||||
if (min > 0)
|
||||
comps.push(`${min} mins`);
|
||||
|
||||
return comps.join(", ") || fallback;
|
||||
}
|
||||
|
||||
text: UPower.displayDevice.isLaptopBattery ? qsTr("Time %1: %2").arg(UPower.onBattery ? "remaining" : "until charged").arg(UPower.onBattery ? formatSeconds(UPower.displayDevice.timeToEmpty, "Calculating...") : formatSeconds(UPower.displayDevice.timeToFull, "Fully charged!")) : qsTr("Power profile: %1").arg(PowerProfile.toString(PowerProfiles.profile))
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None
|
||||
|
||||
height: active ? (item?.implicitHeight ?? 0) : 0
|
||||
|
||||
sourceComponent: StyledRect {
|
||||
implicitWidth: child.implicitWidth + Appearance.padding.normal * 2
|
||||
implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
color: Colours.palette.m3error
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
Column {
|
||||
id: child
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -font.pointSize / 10
|
||||
|
||||
text: "warning"
|
||||
color: Colours.palette.m3onError
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Performance Degraded")
|
||||
color: Colours.palette.m3onError
|
||||
font.family: Appearance.font.family.mono
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -font.pointSize / 10
|
||||
|
||||
text: "warning"
|
||||
color: Colours.palette.m3onError
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: qsTr("Reason: %1").arg(PerformanceDegradationReason.toString(PowerProfiles.degradationReason))
|
||||
color: Colours.palette.m3onError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: profiles
|
||||
|
||||
property string current: {
|
||||
const p = PowerProfiles.profile;
|
||||
if (p === PowerProfile.PowerSaver)
|
||||
return saver.icon;
|
||||
if (p === PowerProfile.Performance)
|
||||
return perf.icon;
|
||||
return balance.icon;
|
||||
}
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2
|
||||
implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2
|
||||
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
StyledRect {
|
||||
id: indicator
|
||||
|
||||
color: Colours.palette.m3primary
|
||||
radius: Appearance.rounding.full
|
||||
state: profiles.current
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: saver.icon
|
||||
|
||||
Fill {
|
||||
item: saver
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: balance.icon
|
||||
|
||||
Fill {
|
||||
item: balance
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: perf.icon
|
||||
|
||||
Fill {
|
||||
item: perf
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Profile {
|
||||
id: saver
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.small
|
||||
|
||||
profile: PowerProfile.PowerSaver
|
||||
icon: "energy_savings_leaf"
|
||||
}
|
||||
|
||||
Profile {
|
||||
id: balance
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
profile: PowerProfile.Balanced
|
||||
icon: "balance"
|
||||
}
|
||||
|
||||
Profile {
|
||||
id: perf
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.small
|
||||
|
||||
profile: PowerProfile.Performance
|
||||
icon: "rocket_launch"
|
||||
}
|
||||
}
|
||||
|
||||
component Fill: AnchorChanges {
|
||||
required property Item item
|
||||
|
||||
target: indicator
|
||||
anchors.left: item.left
|
||||
anchors.right: item.right
|
||||
anchors.top: item.top
|
||||
anchors.bottom: item.bottom
|
||||
}
|
||||
|
||||
component Profile: Item {
|
||||
required property string icon
|
||||
required property int profile
|
||||
|
||||
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.full
|
||||
color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
PowerProfiles.profile = parent.profile;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: parent.icon
|
||||
font.pointSize: Appearance.font.size.large
|
||||
color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
fill: profiles.current === text ? 1 : 0
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
197
.config/quickshell/caelestia/modules/bar/popouts/Bluetooth.qml
Normal file
197
.config/quickshell/caelestia/modules/bar/popouts/Bluetooth.qml
Normal file
@@ -0,0 +1,197 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../controlcenter/network"
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Item wrapper
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.padding.normal
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("Bluetooth")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Enabled")
|
||||
checked: Bluetooth.defaultAdapter?.enabled ?? false
|
||||
toggle.onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.enabled = checked;
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Discovering")
|
||||
checked: Bluetooth.defaultAdapter?.discovering ?? false
|
||||
toggle.onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discovering = checked;
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: {
|
||||
const devices = Bluetooth.devices.values;
|
||||
let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s");
|
||||
const connected = devices.filter(d => d.connected).length;
|
||||
if (connected > 0)
|
||||
available += qsTr(" (%1 connected)").arg(connected);
|
||||
return available;
|
||||
}
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: device
|
||||
|
||||
required property BluetoothDevice modelData
|
||||
readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
opacity: 0
|
||||
scale: 0.7
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
scale = 1;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: Icons.getBluetoothIcon(device.modelData.icon)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.leftMargin: Appearance.spacing.small / 2
|
||||
Layout.rightMargin: Appearance.spacing.small / 2
|
||||
Layout.fillWidth: true
|
||||
text: device.modelData.name
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: connectBtn
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0)
|
||||
|
||||
CircularIndicator {
|
||||
anchors.fill: parent
|
||||
running: device.loading
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
disabled: device.loading
|
||||
|
||||
function onClicked(): void {
|
||||
device.modelData.connected = !device.modelData.connected;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: device.modelData.connected ? "link_off" : "link"
|
||||
color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
opacity: device.loading ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: device.modelData.bonded
|
||||
sourceComponent: Item {
|
||||
implicitWidth: connectBtn.implicitWidth
|
||||
implicitHeight: connectBtn.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
device.modelData.forget();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
text: "delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconTextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
verticalPadding: Appearance.padding.small
|
||||
text: qsTr("Open settings")
|
||||
icon: "settings"
|
||||
|
||||
onClicked: root.wrapper.detach("bluetooth")
|
||||
}
|
||||
|
||||
component Toggle: RowLayout {
|
||||
required property string label
|
||||
property alias checked: toggle.checked
|
||||
property alias toggle: toggle
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: parent.label
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
id: toggle
|
||||
}
|
||||
}
|
||||
}
|
||||
222
.config/quickshell/caelestia/modules/bar/popouts/Content.qml
Normal file
222
.config/quickshell/caelestia/modules/bar/popouts/Content.qml
Normal file
@@ -0,0 +1,222 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
|
||||
import "./kblayout"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Item wrapper
|
||||
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
|
||||
readonly property Item current: currentPopout?.item ?? null
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2
|
||||
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
Popout {
|
||||
name: "activewindow"
|
||||
sourceComponent: ActiveWindow {
|
||||
wrapper: root.wrapper
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
id: networkPopout
|
||||
name: "network"
|
||||
sourceComponent: Network {
|
||||
wrapper: root.wrapper
|
||||
view: "wireless"
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "ethernet"
|
||||
sourceComponent: Network {
|
||||
wrapper: root.wrapper
|
||||
view: "ethernet"
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
id: passwordPopout
|
||||
name: "wirelesspassword"
|
||||
sourceComponent: WirelessPassword {
|
||||
id: passwordComponent
|
||||
wrapper: root.wrapper
|
||||
network: networkPopout.item?.passwordNetwork ?? null
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.wrapper
|
||||
function onCurrentNameChanged() {
|
||||
// Update network immediately when password popout becomes active
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
// Set network immediately if available
|
||||
if (networkPopout.item && networkPopout.item.passwordNetwork) {
|
||||
if (passwordPopout.item) {
|
||||
passwordPopout.item.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
}
|
||||
// Also try after a short delay in case networkPopout.item wasn't ready
|
||||
Qt.callLater(() => {
|
||||
if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) {
|
||||
passwordPopout.item.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: networkPopout
|
||||
function onItemChanged() {
|
||||
// When network popout loads, update password popout if it's active
|
||||
if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) {
|
||||
Qt.callLater(() => {
|
||||
if (networkPopout.item && networkPopout.item.passwordNetwork) {
|
||||
passwordPopout.item.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "bluetooth"
|
||||
sourceComponent: Bluetooth {
|
||||
wrapper: root.wrapper
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "battery"
|
||||
sourceComponent: Battery {}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "audio"
|
||||
sourceComponent: Audio {
|
||||
wrapper: root.wrapper
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "kblayout"
|
||||
sourceComponent: KbLayout {
|
||||
wrapper: root.wrapper
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "lockstatus"
|
||||
sourceComponent: LockStatus {}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id))
|
||||
}
|
||||
|
||||
Popout {
|
||||
id: trayMenu
|
||||
|
||||
required property SystemTrayItem modelData
|
||||
required property int index
|
||||
|
||||
name: `traymenu${index}`
|
||||
sourceComponent: trayMenuComp
|
||||
|
||||
Connections {
|
||||
target: root.wrapper
|
||||
|
||||
function onHasCurrentChanged(): void {
|
||||
if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) {
|
||||
trayMenu.sourceComponent = null;
|
||||
trayMenu.sourceComponent = trayMenuComp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: trayMenuComp
|
||||
|
||||
TrayMenu {
|
||||
popouts: root.wrapper
|
||||
trayItem: trayMenu.modelData.menu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Popout: Loader {
|
||||
id: popout
|
||||
|
||||
required property string name
|
||||
readonly property bool shouldBeActive: root.wrapper.currentName === name
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
opacity: 0
|
||||
scale: 0.8
|
||||
active: false
|
||||
|
||||
states: State {
|
||||
name: "active"
|
||||
when: popout.shouldBeActive
|
||||
|
||||
PropertyChanges {
|
||||
popout.active: true
|
||||
popout.opacity: 1
|
||||
popout.scale: 1
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "active"
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
PropertyAction {
|
||||
target: popout
|
||||
property: "active"
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: ""
|
||||
to: "active"
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
target: popout
|
||||
property: "active"
|
||||
}
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Capslock: %1").arg(Hypr.capsLock ? "Enabled" : "Disabled")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Numlock: %1").arg(Hypr.numLock ? "Enabled" : "Disabled")
|
||||
}
|
||||
}
|
||||
388
.config/quickshell/caelestia/modules/bar/popouts/Network.qml
Normal file
388
.config/quickshell/caelestia/modules/bar/popouts/Network.qml
Normal file
@@ -0,0 +1,388 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Item wrapper
|
||||
|
||||
property string connectingToSsid: ""
|
||||
property string view: "wireless" // "wireless" or "ethernet"
|
||||
property var passwordNetwork: null
|
||||
property bool showPasswordDialog: false
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
width: Config.bar.sizes.networkWidth
|
||||
|
||||
// Wireless section
|
||||
StyledText {
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.padding.normal : 0
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("Wireless")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Toggle {
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
label: qsTr("Enabled")
|
||||
checked: Nmcli.wifiEnabled
|
||||
toggle.onToggled: Nmcli.enableWifi(checked)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.spacing.small : 0
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("%1 networks available").arg(Nmcli.networks.length)
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
Repeater {
|
||||
visible: root.view === "wireless"
|
||||
model: ScriptModel {
|
||||
values: [...Nmcli.networks].sort((a, b) => {
|
||||
if (a.active !== b.active)
|
||||
return b.active - a.active;
|
||||
return b.strength - a.strength;
|
||||
}).slice(0, 8)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: networkItem
|
||||
|
||||
required property Nmcli.AccessPoint modelData
|
||||
readonly property bool isConnecting: root.connectingToSsid === modelData.ssid
|
||||
readonly property bool loading: networkItem.isConnecting
|
||||
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
opacity: 0
|
||||
scale: 0.7
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
scale = 1;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: Icons.getNetworkIcon(networkItem.modelData.strength)
|
||||
color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
visible: networkItem.modelData.isSecure
|
||||
text: "lock"
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.leftMargin: Appearance.spacing.small / 2
|
||||
Layout.rightMargin: Appearance.spacing.small / 2
|
||||
Layout.fillWidth: true
|
||||
text: networkItem.modelData.ssid
|
||||
elide: Text.ElideRight
|
||||
font.weight: networkItem.modelData.active ? 500 : 400
|
||||
color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0)
|
||||
|
||||
CircularIndicator {
|
||||
anchors.fill: parent
|
||||
running: networkItem.loading
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
disabled: networkItem.loading || !Nmcli.wifiEnabled
|
||||
|
||||
function onClicked(): void {
|
||||
if (networkItem.modelData.active) {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
} else {
|
||||
root.connectingToSsid = networkItem.modelData.ssid;
|
||||
NetworkConnection.handleConnect(networkItem.modelData, null, network => {
|
||||
// Password is required - show password dialog
|
||||
root.passwordNetwork = network;
|
||||
root.showPasswordDialog = true;
|
||||
root.wrapper.currentName = "wirelesspassword";
|
||||
});
|
||||
|
||||
// Clear connecting state if connection succeeds immediately (saved profile)
|
||||
// This is handled by the onActiveChanged connection below
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: wirelessConnectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: networkItem.modelData.active ? "link_off" : "link"
|
||||
color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
opacity: networkItem.loading ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
visible: root.view === "wireless"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.spacing.small : 0
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primaryContainer
|
||||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
disabled: Nmcli.scanning || !Nmcli.wifiEnabled
|
||||
|
||||
function onClicked(): void {
|
||||
Nmcli.rescanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rescanBtn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.small
|
||||
opacity: Nmcli.scanning ? 0 : 1
|
||||
|
||||
MaterialIcon {
|
||||
id: scanIcon
|
||||
|
||||
Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)
|
||||
animate: true
|
||||
text: "wifi_find"
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: -Math.round(scanIcon.fontInfo.pointSize * 0.0575)
|
||||
text: qsTr("Rescan networks")
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
CircularIndicator {
|
||||
anchors.centerIn: parent
|
||||
strokeWidth: Appearance.padding.small / 2
|
||||
bgColour: "transparent"
|
||||
implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2
|
||||
running: Nmcli.scanning
|
||||
}
|
||||
}
|
||||
|
||||
// Ethernet section
|
||||
StyledText {
|
||||
visible: root.view === "ethernet"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.padding.normal : 0
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("Ethernet")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.view === "ethernet"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.topMargin: visible ? Appearance.spacing.small : 0
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length)
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
Repeater {
|
||||
visible: root.view === "ethernet"
|
||||
model: ScriptModel {
|
||||
values: [...Nmcli.ethernetDevices].sort((a, b) => {
|
||||
if (a.connected !== b.connected)
|
||||
return b.connected - a.connected;
|
||||
return (a.interface || "").localeCompare(b.interface || "");
|
||||
}).slice(0, 8)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: ethernetItem
|
||||
|
||||
required property var modelData
|
||||
readonly property bool loading: false
|
||||
|
||||
visible: root.view === "ethernet"
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
opacity: 0
|
||||
scale: 0.7
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
scale = 1;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "cable"
|
||||
color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.leftMargin: Appearance.spacing.small / 2
|
||||
Layout.rightMargin: Appearance.spacing.small / 2
|
||||
Layout.fillWidth: true
|
||||
text: ethernetItem.modelData.interface || qsTr("Unknown")
|
||||
elide: Text.ElideRight
|
||||
font.weight: ethernetItem.modelData.connected ? 500 : 400
|
||||
color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0)
|
||||
|
||||
CircularIndicator {
|
||||
anchors.fill: parent
|
||||
running: ethernetItem.loading
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
disabled: ethernetItem.loading
|
||||
|
||||
function onClicked(): void {
|
||||
if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) {
|
||||
Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {});
|
||||
} else {
|
||||
Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: ethernetItem.modelData.connected ? "link_off" : "link"
|
||||
color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
opacity: ethernetItem.loading ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Nmcli
|
||||
|
||||
function onActiveChanged(): void {
|
||||
if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) {
|
||||
root.connectingToSsid = "";
|
||||
// Close password dialog if we successfully connected
|
||||
if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) {
|
||||
root.showPasswordDialog = false;
|
||||
root.passwordNetwork = null;
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
root.wrapper.currentName = "network";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onScanningChanged(): void {
|
||||
if (!Nmcli.scanning)
|
||||
scanIcon.rotation = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.wrapper
|
||||
function onCurrentNameChanged(): void {
|
||||
// Clear password network when leaving password dialog
|
||||
if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) {
|
||||
root.showPasswordDialog = false;
|
||||
root.passwordNetwork = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Toggle: RowLayout {
|
||||
required property string label
|
||||
property alias checked: toggle.checked
|
||||
property alias toggle: toggle
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: parent.label
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
id: toggle
|
||||
}
|
||||
}
|
||||
}
|
||||
225
.config/quickshell/caelestia/modules/bar/popouts/TrayMenu.qml
Normal file
225
.config/quickshell/caelestia/modules/bar/popouts/TrayMenu.qml
Normal file
@@ -0,0 +1,225 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
StackView {
|
||||
id: root
|
||||
|
||||
required property Item popouts
|
||||
required property QsMenuHandle trayItem
|
||||
|
||||
implicitWidth: currentItem.implicitWidth
|
||||
implicitHeight: currentItem.implicitHeight
|
||||
|
||||
initialItem: SubMenu {
|
||||
handle: root.trayItem
|
||||
}
|
||||
|
||||
pushEnter: NoAnim {}
|
||||
pushExit: NoAnim {}
|
||||
popEnter: NoAnim {}
|
||||
popExit: NoAnim {}
|
||||
|
||||
component NoAnim: Transition {
|
||||
NumberAnimation {
|
||||
duration: 0
|
||||
}
|
||||
}
|
||||
|
||||
component SubMenu: Column {
|
||||
id: menu
|
||||
|
||||
required property QsMenuHandle handle
|
||||
property bool isSubMenu
|
||||
property bool shown
|
||||
|
||||
padding: Appearance.padding.smaller
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
opacity: shown ? 1 : 0
|
||||
scale: shown ? 1 : 0.8
|
||||
|
||||
Component.onCompleted: shown = true
|
||||
StackView.onActivating: shown = true
|
||||
StackView.onDeactivating: shown = false
|
||||
StackView.onRemoved: destroy()
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: menuOpener
|
||||
|
||||
menu: menu.handle
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: menuOpener.children
|
||||
|
||||
StyledRect {
|
||||
id: item
|
||||
|
||||
required property QsMenuEntry modelData
|
||||
|
||||
implicitWidth: Config.bar.sizes.trayMenuWidth
|
||||
implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent"
|
||||
|
||||
Loader {
|
||||
id: children
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
active: !item.modelData.isSeparator
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitHeight: label.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
anchors.margins: -Appearance.padding.small / 2
|
||||
anchors.leftMargin: -Appearance.padding.smaller
|
||||
anchors.rightMargin: -Appearance.padding.smaller
|
||||
|
||||
radius: item.radius
|
||||
disabled: !item.modelData.enabled
|
||||
|
||||
function onClicked(): void {
|
||||
const entry = item.modelData;
|
||||
if (entry.hasChildren)
|
||||
root.push(subMenuComp.createObject(null, {
|
||||
handle: entry,
|
||||
isSubMenu: true
|
||||
}));
|
||||
else {
|
||||
item.modelData.triggered();
|
||||
root.popouts.hasCurrent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: icon
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
active: item.modelData.icon !== ""
|
||||
|
||||
sourceComponent: IconImage {
|
||||
implicitSize: label.implicitHeight
|
||||
|
||||
source: item.modelData.icon
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0
|
||||
|
||||
text: labelMetrics.elidedText
|
||||
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: labelMetrics
|
||||
|
||||
text: item.modelData.text
|
||||
font.pointSize: label.font.pointSize
|
||||
font.family: label.font.family
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: expand
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
active: item.modelData.hasChildren
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "chevron_right"
|
||||
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: menu.isSubMenu
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitWidth: back.implicitWidth
|
||||
implicitHeight: back.implicitHeight + Appearance.spacing.small / 2
|
||||
|
||||
Item {
|
||||
anchors.bottom: parent.bottom
|
||||
implicitWidth: back.implicitWidth
|
||||
implicitHeight: back.implicitHeight
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -Appearance.padding.small / 2
|
||||
anchors.leftMargin: -Appearance.padding.smaller
|
||||
anchors.rightMargin: -Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
radius: parent.radius
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
|
||||
function onClicked(): void {
|
||||
root.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: back
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MaterialIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "chevron_left"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Back")
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: subMenuComp
|
||||
|
||||
SubMenu {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Item wrapper
|
||||
property var network: null
|
||||
property bool isClosing: false
|
||||
|
||||
readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword"
|
||||
|
||||
Connections {
|
||||
target: root.wrapper
|
||||
function onCurrentNameChanged() {
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
// Update network when popout becomes active
|
||||
Qt.callLater(() => {
|
||||
// Try to get network from parent Content's networkPopout
|
||||
const content = root.parent?.parent?.parent;
|
||||
if (content) {
|
||||
const networkPopout = content.children.find(c => c.name === "network");
|
||||
if (networkPopout && networkPopout.item) {
|
||||
root.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
}
|
||||
// Force focus to password container when popout becomes active
|
||||
// Use Timer for actual delay to ensure dialog is fully rendered
|
||||
focusTimer.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: focusTimer
|
||||
interval: 150
|
||||
onTriggered: {
|
||||
root.forceActiveFocus();
|
||||
passwordContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
visible: shouldBeVisible || isClosing
|
||||
enabled: shouldBeVisible && !isClosing
|
||||
focus: enabled
|
||||
|
||||
Component.onCompleted: {
|
||||
if (shouldBeVisible) {
|
||||
// Use Timer for actual delay to ensure dialog is fully rendered
|
||||
focusTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
// Use Timer for actual delay to ensure dialog is fully rendered
|
||||
focusTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: closeDialog()
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 400
|
||||
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
visible: root.shouldBeVisible || root.isClosing
|
||||
opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0
|
||||
scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: root.isClosing
|
||||
onFinished: {
|
||||
if (root.isClosing) {
|
||||
root.isClosing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: parent
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: parent
|
||||
property: "scale"
|
||||
to: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: root.closeDialog()
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "lock"
|
||||
font.pointSize: Appearance.font.size.extraLarge * 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Enter password")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: networkNameText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
if (root.network) {
|
||||
const ssid = root.network.ssid;
|
||||
if (ssid && ssid.length > 0) {
|
||||
return qsTr("Network: %1").arg(ssid);
|
||||
}
|
||||
}
|
||||
return qsTr("Network: Unknown");
|
||||
}
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 50
|
||||
running: root.shouldBeVisible && (!root.network || !root.network.ssid)
|
||||
repeat: true
|
||||
property int attempts: 0
|
||||
onTriggered: {
|
||||
attempts++;
|
||||
// Keep trying to get network from Network component
|
||||
const content = root.parent?.parent?.parent;
|
||||
if (content) {
|
||||
const networkPopout = content.children.find(c => c.name === "network");
|
||||
if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) {
|
||||
root.network = networkPopout.item.passwordNetwork;
|
||||
}
|
||||
}
|
||||
// Stop if we got it or after 20 attempts (1 second)
|
||||
if ((root.network && root.network.ssid) || attempts >= 20) {
|
||||
stop();
|
||||
attempts = 0;
|
||||
}
|
||||
}
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
attempts = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: statusText
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
visible: connectButton.connecting || connectButton.hasError
|
||||
text: {
|
||||
if (connectButton.hasError) {
|
||||
return qsTr("Connection failed. Please check your password and try again.");
|
||||
}
|
||||
if (connectButton.connecting) {
|
||||
return qsTr("Connecting...");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: 400
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.maximumWidth: parent.width - Appearance.padding.large * 2
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: passwordContainer
|
||||
objectName: "passwordContainer"
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)
|
||||
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
|
||||
property string passwordBuffer: ""
|
||||
|
||||
Keys.onPressed: event => {
|
||||
// Ensure we have focus when receiving keyboard input
|
||||
if (!activeFocus) {
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (connectButton.hasError && event.text && event.text.length > 0) {
|
||||
connectButton.hasError = false;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
if (connectButton.enabled) {
|
||||
connectButton.clicked();
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
passwordBuffer = "";
|
||||
} else {
|
||||
passwordBuffer = passwordBuffer.slice(0, -1);
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.text && event.text.length > 0) {
|
||||
passwordBuffer += event.text;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged(): void {
|
||||
if (root.shouldBeVisible) {
|
||||
// Use Timer for actual delay to ensure focus works correctly
|
||||
passwordFocusTimer.start();
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.hasError = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: passwordFocusTimer
|
||||
interval: 50
|
||||
onTriggered: {
|
||||
passwordContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldBeVisible) {
|
||||
// Use Timer for actual delay to ensure focus works correctly
|
||||
passwordFocusTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.normal
|
||||
color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer
|
||||
border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0)
|
||||
border.color: {
|
||||
if (connectButton.hasError) {
|
||||
return Colours.palette.m3error;
|
||||
}
|
||||
if (passwordContainer.activeFocus) {
|
||||
return Colours.palette.m3primary;
|
||||
}
|
||||
return root.shouldBeVisible ? Colours.palette.m3outline : "transparent";
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
hoverEnabled: false
|
||||
cursorShape: Qt.IBeamCursor
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
function onClicked(): void {
|
||||
passwordContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeholder
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Password")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.family: Appearance.font.family.mono
|
||||
opacity: passwordContainer.passwordBuffer ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: charList
|
||||
|
||||
readonly property int fullWidth: count * (implicitHeight + spacing) - spacing
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: fullWidth
|
||||
implicitHeight: Appearance.font.size.normal
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Appearance.spacing.small / 2
|
||||
interactive: false
|
||||
|
||||
model: ScriptModel {
|
||||
values: passwordContainer.passwordBuffer.split("")
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
id: ch
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: charList.implicitHeight
|
||||
|
||||
color: Colours.palette.m3onSurface
|
||||
radius: Appearance.rounding.small / 2
|
||||
|
||||
opacity: 0
|
||||
scale: 0
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
scale = 1;
|
||||
}
|
||||
ListView.onRemove: removeAnim.start()
|
||||
|
||||
SequentialAnimation {
|
||||
id: removeAnim
|
||||
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: true
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: ch
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: ch
|
||||
property: "scale"
|
||||
to: 0.5
|
||||
}
|
||||
}
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
id: cancelButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.closeDialog()
|
||||
}
|
||||
|
||||
TextButton {
|
||||
id: connectButton
|
||||
|
||||
property bool connecting: false
|
||||
property bool hasError: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3primary
|
||||
inactiveOnColour: Colours.palette.m3onPrimary
|
||||
text: qsTr("Connect")
|
||||
enabled: passwordContainer.passwordBuffer.length > 0 && !connecting
|
||||
|
||||
onClicked: {
|
||||
if (!root.network || connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = passwordContainer.passwordBuffer;
|
||||
if (!password || password.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any previous error
|
||||
hasError = false;
|
||||
|
||||
// Set connecting state
|
||||
connecting = true;
|
||||
enabled = false;
|
||||
text = qsTr("Connecting...");
|
||||
|
||||
// Connect to network
|
||||
NetworkConnection.connectWithPassword(root.network, password, result => {
|
||||
if (result && result.success)
|
||||
// Connection successful, monitor will handle the rest
|
||||
{} else if (result && result.needsPassword) {
|
||||
// Shouldn't happen since we provided password
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
// Delete the failed connection
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
} else {
|
||||
// Connection failed immediately - show error
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
// Delete the failed connection
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start monitoring connection
|
||||
connectionMonitor.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkConnectionStatus(): void {
|
||||
if (!root.shouldBeVisible || !connectButton.connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're connected to the target network (case-insensitive SSID comparison)
|
||||
const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
|
||||
if (isConnected) {
|
||||
// Successfully connected - give it a moment for network list to update
|
||||
// Use Timer for actual delay
|
||||
connectionSuccessTimer.start();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for connection failures - if pending connection was cleared but we're not connected
|
||||
if (Nmcli.pendingConnection === null && connectButton.connecting) {
|
||||
// Wait a bit more before giving up (allow time for connection to establish)
|
||||
if (connectionMonitor.repeatCount > 10) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
// Delete the failed connection
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionMonitor
|
||||
interval: 1000
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
property int repeatCount: 0
|
||||
|
||||
onTriggered: {
|
||||
repeatCount++;
|
||||
root.checkConnectionStatus();
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
repeatCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionSuccessTimer
|
||||
interval: 500
|
||||
onTriggered: {
|
||||
// Double-check connection is still active
|
||||
if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) {
|
||||
const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
if (stillConnected) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
// Return to network popout on successful connection
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
root.wrapper.currentName = "network";
|
||||
}
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Nmcli
|
||||
function onActiveChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
root.checkConnectionStatus();
|
||||
}
|
||||
}
|
||||
function onConnectionFailed(ssid: string) {
|
||||
if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
// Delete the failed connection
|
||||
Nmcli.forgetNetwork(ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog(): void {
|
||||
if (isClosing) {
|
||||
return;
|
||||
}
|
||||
|
||||
isClosing = true;
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
connectionMonitor.stop();
|
||||
|
||||
// Return to network popout
|
||||
if (root.wrapper.currentName === "wirelesspassword") {
|
||||
root.wrapper.currentName = "network";
|
||||
}
|
||||
}
|
||||
}
|
||||
215
.config/quickshell/caelestia/modules/bar/popouts/Wrapper.qml
Normal file
215
.config/quickshell/caelestia/modules/bar/popouts/Wrapper.qml
Normal file
@@ -0,0 +1,215 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.windowinfo
|
||||
import qs.modules.controlcenter
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0
|
||||
readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight
|
||||
readonly property Item current: content.item?.current ?? null
|
||||
|
||||
property string currentName
|
||||
property real currentCenter
|
||||
property bool hasCurrent
|
||||
|
||||
property string detachedMode
|
||||
property string queuedMode
|
||||
readonly property bool isDetached: detachedMode.length > 0
|
||||
|
||||
property int animLength: Appearance.anim.durations.normal
|
||||
property list<real> animCurve: Appearance.anim.curves.emphasized
|
||||
|
||||
function detach(mode: string): void {
|
||||
animLength = Appearance.anim.durations.large;
|
||||
if (mode === "winfo") {
|
||||
detachedMode = mode;
|
||||
} else {
|
||||
queuedMode = mode;
|
||||
detachedMode = "any";
|
||||
}
|
||||
focus = true;
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
hasCurrent = false;
|
||||
animCurve = Appearance.anim.curves.emphasizedAccel;
|
||||
animLength = Appearance.anim.durations.normal;
|
||||
detachedMode = "";
|
||||
animCurve = Appearance.anim.curves.emphasized;
|
||||
}
|
||||
|
||||
visible: width > 0 && height > 0
|
||||
clip: true
|
||||
|
||||
implicitWidth: nonAnimWidth
|
||||
implicitHeight: nonAnimHeight
|
||||
|
||||
focus: hasCurrent
|
||||
Keys.onEscapePressed: {
|
||||
// Forward escape to password popout if active, otherwise close
|
||||
if (currentName === "wirelesspassword" && content.item) {
|
||||
const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword");
|
||||
if (passwordPopout && passwordPopout.item) {
|
||||
passwordPopout.item.closeDialog();
|
||||
return;
|
||||
}
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
// Don't intercept keys when password popout is active - let it handle them
|
||||
if (currentName === "wirelesspassword") {
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: root.isDetached
|
||||
windows: [QsWindow.window]
|
||||
onCleared: root.close()
|
||||
}
|
||||
|
||||
Binding {
|
||||
when: root.isDetached
|
||||
|
||||
target: QsWindow.window
|
||||
property: "WlrLayershell.keyboardFocus"
|
||||
value: WlrKeyboardFocus.OnDemand
|
||||
}
|
||||
|
||||
Binding {
|
||||
when: root.hasCurrent && root.currentName === "wirelesspassword"
|
||||
|
||||
target: QsWindow.window
|
||||
property: "WlrLayershell.keyboardFocus"
|
||||
value: WlrKeyboardFocus.OnDemand
|
||||
}
|
||||
|
||||
Comp {
|
||||
id: content
|
||||
|
||||
shouldBeActive: root.hasCurrent && !root.detachedMode
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
sourceComponent: Content {
|
||||
wrapper: root
|
||||
}
|
||||
}
|
||||
|
||||
Comp {
|
||||
shouldBeActive: root.detachedMode === "winfo"
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: WindowInfo {
|
||||
screen: root.screen
|
||||
client: Hypr.activeToplevel
|
||||
}
|
||||
}
|
||||
|
||||
Comp {
|
||||
shouldBeActive: root.detachedMode === "any"
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: ControlCenter {
|
||||
screen: root.screen
|
||||
active: root.queuedMode
|
||||
|
||||
function close(): void {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
duration: root.animLength
|
||||
easing.bezierCurve: root.animCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
enabled: root.implicitWidth > 0
|
||||
|
||||
Anim {
|
||||
duration: root.animLength
|
||||
easing.bezierCurve: root.animCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
duration: root.animLength
|
||||
easing.bezierCurve: root.animCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: root.implicitWidth > 0
|
||||
|
||||
Anim {
|
||||
duration: root.animLength
|
||||
easing.bezierCurve: root.animCurve
|
||||
}
|
||||
}
|
||||
|
||||
component Comp: Loader {
|
||||
id: comp
|
||||
|
||||
property bool shouldBeActive
|
||||
|
||||
active: false
|
||||
opacity: 0
|
||||
|
||||
states: State {
|
||||
name: "active"
|
||||
when: comp.shouldBeActive
|
||||
|
||||
PropertyChanges {
|
||||
comp.opacity: 1
|
||||
comp.active: true
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "active"
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "active"
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
}
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
|
||||
import "."
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Item wrapper
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
width: Config.bar.sizes.kbLayoutWidth
|
||||
|
||||
KbLayoutModel {
|
||||
id: kb
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
kb.refresh();
|
||||
}
|
||||
Component.onCompleted: kb.start()
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.padding.normal
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
text: qsTr("Keyboard Layouts")
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: list
|
||||
model: kb.visibleModel
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
|
||||
clip: true
|
||||
interactive: true
|
||||
implicitHeight: Math.min(contentHeight, 320)
|
||||
visible: kb.visibleModel.count > 0
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
add: Transition {
|
||||
NumberAnimation {
|
||||
properties: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 140
|
||||
}
|
||||
NumberAnimation {
|
||||
properties: "y"
|
||||
duration: 180
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
NumberAnimation {
|
||||
properties: "opacity"
|
||||
to: 0
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
NumberAnimation {
|
||||
properties: "y"
|
||||
duration: 180
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
displaced: Transition {
|
||||
NumberAnimation {
|
||||
properties: "y"
|
||||
duration: 180
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
required property int layoutIndex
|
||||
required property string label
|
||||
|
||||
width: list.width
|
||||
height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2)
|
||||
|
||||
readonly property bool isDisabled: layoutIndex > 3
|
||||
|
||||
StateLayer {
|
||||
id: layer
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitHeight: parent.height - 4
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
enabled: !isDisabled
|
||||
|
||||
function onClicked(): void {
|
||||
if (!isDisabled)
|
||||
kb.switchTo(layoutIndex);
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: rowText
|
||||
anchors.verticalCenter: layer.verticalCenter
|
||||
anchors.left: layer.left
|
||||
anchors.right: layer.right
|
||||
anchors.leftMargin: Appearance.padding.small
|
||||
anchors.rightMargin: Appearance.padding.small
|
||||
text: label
|
||||
elide: Text.ElideRight
|
||||
opacity: isDisabled ? 0.4 : 1.0
|
||||
}
|
||||
|
||||
ToolTip.visible: isDisabled && layer.containsMouse
|
||||
ToolTip.text: "XKB limitation: maximum 4 layouts allowed"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: kb.activeLabel.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
|
||||
height: 1
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
opacity: 0.35
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: activeRow
|
||||
|
||||
visible: kb.activeLabel.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.padding.small
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
|
||||
MaterialIcon {
|
||||
text: "keyboard"
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: kb.activeLabel
|
||||
elide: Text.ElideRight
|
||||
font.weight: 500
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: kb
|
||||
function onActiveLabelChanged() {
|
||||
if (!activeRow.visible)
|
||||
return;
|
||||
popIn.restart();
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: popIn
|
||||
running: false
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: activeRow
|
||||
property: "opacity"
|
||||
to: 0.0
|
||||
duration: 70
|
||||
}
|
||||
NumberAnimation {
|
||||
target: activeRow
|
||||
property: "scale"
|
||||
to: 0.92
|
||||
duration: 70
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: activeRow
|
||||
property: "opacity"
|
||||
to: 1.0
|
||||
duration: 160
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: activeRow
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
duration: 220
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
import qs.config
|
||||
import Caelestia
|
||||
|
||||
Item {
|
||||
id: model
|
||||
visible: false
|
||||
|
||||
ListModel {
|
||||
id: _visibleModel
|
||||
}
|
||||
property alias visibleModel: _visibleModel
|
||||
|
||||
property string activeLabel: ""
|
||||
property int activeIndex: -1
|
||||
|
||||
function start() {
|
||||
_xkbXmlBase.running = true;
|
||||
_getKbLayoutOpt.running = true;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
_notifiedLimit = false;
|
||||
_getKbLayoutOpt.running = true;
|
||||
}
|
||||
|
||||
function switchTo(idx) {
|
||||
_switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)];
|
||||
_switchProc.running = true;
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: _layoutsModel
|
||||
}
|
||||
|
||||
property var _xkbMap: ({})
|
||||
property bool _notifiedLimit: false
|
||||
|
||||
Process {
|
||||
id: _xkbXmlBase
|
||||
command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: _buildXmlMap(text)
|
||||
}
|
||||
onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0)
|
||||
_xkbXmlEvdev.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: _xkbXmlEvdev
|
||||
command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: _buildXmlMap(text)
|
||||
}
|
||||
}
|
||||
|
||||
function _buildXmlMap(xml) {
|
||||
const map = {};
|
||||
|
||||
const re = /<name>\s*([^<]+?)\s*<\/name>[\s\S]*?<description>\s*([^<]+?)\s*<\/description>/g;
|
||||
|
||||
let m;
|
||||
while ((m = re.exec(xml)) !== null) {
|
||||
const code = (m[1] || "").trim();
|
||||
const desc = (m[2] || "").trim();
|
||||
if (!code || !desc)
|
||||
continue;
|
||||
map[code] = _short(desc);
|
||||
}
|
||||
|
||||
if (Object.keys(map).length === 0)
|
||||
return;
|
||||
|
||||
_xkbMap = map;
|
||||
|
||||
if (_layoutsModel.count > 0) {
|
||||
const tmp = [];
|
||||
for (let i = 0; i < _layoutsModel.count; i++) {
|
||||
const it = _layoutsModel.get(i);
|
||||
tmp.push({
|
||||
layoutIndex: it.layoutIndex,
|
||||
token: it.token,
|
||||
label: _pretty(it.token)
|
||||
});
|
||||
}
|
||||
_layoutsModel.clear();
|
||||
tmp.forEach(t => _layoutsModel.append(t));
|
||||
_fetchActiveLayouts.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
function _short(desc) {
|
||||
const m = desc.match(/^(.*)\((.*)\)$/);
|
||||
if (!m)
|
||||
return desc;
|
||||
const lang = m[1].trim();
|
||||
const region = m[2].trim();
|
||||
const code = (region.split(/[,\s-]/)[0] || region).slice(0, 2).toUpperCase();
|
||||
return `${lang} (${code})`;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: _getKbLayoutOpt
|
||||
command: ["hyprctl", "-j", "getoption", "input:kb_layout"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
const raw = (j?.str || j?.value || "").toString().trim();
|
||||
if (raw.length) {
|
||||
_setLayouts(raw);
|
||||
_fetchActiveLayouts.running = true;
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
_fetchLayoutsFromDevices.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: _fetchLayoutsFromDevices
|
||||
command: ["hyprctl", "-j", "devices"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const dev = JSON.parse(text);
|
||||
const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0];
|
||||
const raw = (kb?.layout || "").trim();
|
||||
if (raw.length)
|
||||
_setLayouts(raw);
|
||||
} catch (e) {}
|
||||
_fetchActiveLayouts.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: _fetchActiveLayouts
|
||||
command: ["hyprctl", "-j", "devices"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const dev = JSON.parse(text);
|
||||
const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0];
|
||||
const idx = kb?.active_layout_index ?? -1;
|
||||
|
||||
activeIndex = idx >= 0 ? idx : -1;
|
||||
activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : "";
|
||||
} catch (e) {
|
||||
activeIndex = -1;
|
||||
activeLabel = "";
|
||||
}
|
||||
|
||||
_rebuildVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: _switchProc
|
||||
onRunningChanged: if (!running)
|
||||
_fetchActiveLayouts.running = true
|
||||
}
|
||||
|
||||
function _setLayouts(raw) {
|
||||
const parts = raw.split(",").map(s => s.trim()).filter(Boolean);
|
||||
_layoutsModel.clear();
|
||||
|
||||
const seen = new Set();
|
||||
let idx = 0;
|
||||
|
||||
for (const p of parts) {
|
||||
if (seen.has(p))
|
||||
continue;
|
||||
seen.add(p);
|
||||
_layoutsModel.append({
|
||||
layoutIndex: idx,
|
||||
token: p,
|
||||
label: _pretty(p)
|
||||
});
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
function _rebuildVisible() {
|
||||
_visibleModel.clear();
|
||||
|
||||
let arr = [];
|
||||
for (let i = 0; i < _layoutsModel.count; i++)
|
||||
arr.push(_layoutsModel.get(i));
|
||||
|
||||
arr = arr.filter(i => i.layoutIndex !== activeIndex);
|
||||
arr.forEach(i => _visibleModel.append(i));
|
||||
|
||||
if (!Config.utilities.toasts.kbLimit)
|
||||
return;
|
||||
|
||||
if (_layoutsModel.count > 4) {
|
||||
Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning");
|
||||
}
|
||||
}
|
||||
|
||||
function _pretty(token) {
|
||||
const code = token.replace(/\(.*\)$/, "").trim();
|
||||
if (_xkbMap[code])
|
||||
return code.toUpperCase() + " - " + _xkbMap[code];
|
||||
return code.toUpperCase() + " - " + code;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user