Initial commit
This commit is contained in:
commit
a8c2f88654
53 changed files with 9269 additions and 0 deletions
67
Bar/Modules/ActiveWindow.qml
Normal file
67
Bar/Modules/ActiveWindow.qml
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Components
|
||||
import qs.Settings
|
||||
|
||||
Item {
|
||||
id: activeWindowWrapper
|
||||
width: parent.width
|
||||
property int fullHeight: activeWindowTitleContainer.height
|
||||
|
||||
y: panel.activeWindowVisible ? barBackground.height : barBackground.height - fullHeight
|
||||
height: panel.activeWindowVisible ? fullHeight : 1
|
||||
opacity: panel.activeWindowVisible ? 1 : 0
|
||||
clip: true
|
||||
|
||||
Behavior on height { NumberAnimation { duration: 300; easing.type: Easing.OutQuad } }
|
||||
Behavior on y { NumberAnimation { duration: 300; easing.type: Easing.OutQuad } }
|
||||
Behavior on opacity { NumberAnimation { duration: 250 } }
|
||||
|
||||
Rectangle {
|
||||
id: activeWindowTitleContainer
|
||||
color: Theme.backgroundPrimary
|
||||
bottomLeftRadius: Math.max(0, width / 2)
|
||||
bottomRightRadius: Math.max(0, width / 2)
|
||||
|
||||
width: Math.min(barBackground.width - 200, activeWindowTitle.implicitWidth + 24)
|
||||
height: activeWindowTitle.implicitHeight + 12
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Text {
|
||||
id: activeWindowTitle
|
||||
text: panel.displayedWindowTitle && panel.displayedWindowTitle.length > 60
|
||||
? panel.displayedWindowTitle.substring(0, 60) + "..."
|
||||
: panel.displayedWindowTitle
|
||||
font.pixelSize: 12
|
||||
color: Theme.textSecondary
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
Corners {
|
||||
id: activeCornerRight
|
||||
position: "bottomleft"
|
||||
size: 1.1
|
||||
fillColor: Theme.backgroundPrimary
|
||||
offsetX: activeWindowTitleContainer.x + activeWindowTitleContainer.width - 33
|
||||
offsetY: 0
|
||||
anchors.top: activeWindowTitleContainer.top
|
||||
}
|
||||
|
||||
Corners {
|
||||
id: activeCornerLeft
|
||||
position: "bottomright"
|
||||
size: 1.1
|
||||
fillColor: Theme.backgroundPrimary
|
||||
anchors.top: activeWindowTitleContainer.top
|
||||
x: activeWindowTitleContainer.x + 33 - width
|
||||
offsetY: 0
|
||||
}
|
||||
}
|
||||
364
Bar/Modules/Applauncher.qml
Normal file
364
Bar/Modules/Applauncher.qml
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Components
|
||||
import qs.Settings
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Wayland
|
||||
import "../../Helpers/Fuzzysort.js" as Fuzzysort
|
||||
|
||||
PanelWindow {
|
||||
id: appLauncherPanel
|
||||
implicitWidth: 460
|
||||
implicitHeight: 640
|
||||
color: "transparent"
|
||||
visible: false
|
||||
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
screen: (typeof modelData !== 'undefined' ? modelData : null)
|
||||
property bool shouldBeVisible: false
|
||||
|
||||
anchors.top: true
|
||||
margins.top: -26
|
||||
|
||||
function showAt() {
|
||||
visible = true;
|
||||
shouldBeVisible = true;
|
||||
searchField.forceActiveFocus()
|
||||
}
|
||||
|
||||
function hidePanel() {
|
||||
shouldBeVisible = false;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
width: 400
|
||||
height: 640
|
||||
x: (parent.width - width) / 2 // Horizontally centered
|
||||
color: Theme.backgroundPrimary
|
||||
bottomLeftRadius: 28
|
||||
bottomRightRadius: 28
|
||||
|
||||
property var appModel: DesktopEntries.applications.values
|
||||
property var filteredApps: []
|
||||
property int selectedIndex: 0
|
||||
property int targetY: (parent.height - height) / 2 // Centered vertically
|
||||
y: appLauncherPanel.shouldBeVisible ? targetY : -height // Slide from above
|
||||
Behavior on y {
|
||||
NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
|
||||
}
|
||||
scale: appLauncherPanel.shouldBeVisible ? 1 : 0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
onScaleChanged: {
|
||||
if (scale === 0 && !appLauncherPanel.shouldBeVisible) {
|
||||
appLauncherPanel.visible = false;
|
||||
}
|
||||
}
|
||||
function isMathExpression(str) {
|
||||
// Allow numbers, operators, parentheses, decimal points, spaces
|
||||
return /^[-+*/().0-9\s]+$/.test(str);
|
||||
}
|
||||
function safeEval(expr) {
|
||||
// Only allow safe math expressions
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
return Function('return (' + expr + ')')();
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
function updateFilter() {
|
||||
var query = searchField.text ? searchField.text.toLowerCase() : "";
|
||||
var apps = root.appModel;
|
||||
var results = [];
|
||||
// Calculator mode: starts with '='
|
||||
if (query.startsWith("=")) {
|
||||
var expr = searchField.text.slice(1).trim();
|
||||
if (expr && isMathExpression(expr)) {
|
||||
var value = safeEval(expr);
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
results.push({
|
||||
isCalculator: true,
|
||||
name: `Calculator: ${expr} = ${value}`,
|
||||
result: value,
|
||||
expr: expr,
|
||||
icon: "calculate"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Normal app search
|
||||
if (!query || query.startsWith("=")) {
|
||||
results = results.concat(apps);
|
||||
} else {
|
||||
var fuzzyResults = Fuzzysort.go(query, apps, { keys: ["name", "comment", "genericName"] });
|
||||
results = results.concat(fuzzyResults.map(function(r) { return r.obj; }));
|
||||
}
|
||||
root.filteredApps = results;
|
||||
root.selectedIndex = 0; // reset selection
|
||||
}
|
||||
function selectNext() {
|
||||
if (filteredApps.length > 0)
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1);
|
||||
}
|
||||
function selectPrev() {
|
||||
if (filteredApps.length > 0)
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||
}
|
||||
function activateSelected() {
|
||||
if (filteredApps.length === 0)
|
||||
return;
|
||||
var modelData = filteredApps[selectedIndex];
|
||||
if (modelData.isCalculator) {
|
||||
Qt.callLater(function() {
|
||||
Quickshell.clipboardText = String(modelData.result);
|
||||
Quickshell.execDetached([
|
||||
"notify-send",
|
||||
"Calculator Result",
|
||||
`${modelData.expr} = ${modelData.result} (copied to clipboard)`
|
||||
]);
|
||||
});
|
||||
} else if (modelData.execString) {
|
||||
Quickshell.execDetached(["sh", "-c", modelData.execString]);
|
||||
} else if (modelData.exec) {
|
||||
Quickshell.execDetached(["sh", "-c", modelData.exec]);
|
||||
} else {
|
||||
if (!modelData.isCalculator)
|
||||
console.warn("Cannot launch app:", modelData.name, "missing execString or exec", modelData);
|
||||
}
|
||||
appLauncherPanel.hidePanel();
|
||||
}
|
||||
Component.onCompleted: updateFilter()
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 32
|
||||
spacing: 18
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1.5
|
||||
color: Theme.outline
|
||||
opacity: 0.10
|
||||
}
|
||||
// Search Bar
|
||||
Rectangle {
|
||||
id: searchBar
|
||||
color: Theme.surfaceVariant
|
||||
radius: 22
|
||||
height: 48
|
||||
Layout.fillWidth: true
|
||||
//border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||
//border.width: searchField.activeFocus ? 2.5 : 1.5
|
||||
layer.enabled: searchField.activeFocus
|
||||
layer.effect: DropShadow {
|
||||
color: Theme.accentPrimary
|
||||
radius: 12
|
||||
samples: 16
|
||||
verticalOffset: 0
|
||||
horizontalOffset: 0
|
||||
opacity: 0.10
|
||||
}
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
anchors.margins: 14
|
||||
Text {
|
||||
text: "search"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 22
|
||||
color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
TextField {
|
||||
id: searchField
|
||||
placeholderText: "Search apps..."
|
||||
color: Theme.textPrimary
|
||||
placeholderTextColor: Theme.textSecondary
|
||||
background: null
|
||||
font.pixelSize: 17
|
||||
Layout.fillWidth: true
|
||||
onTextChanged: root.updateFilter()
|
||||
selectedTextColor: Theme.onAccent
|
||||
selectionColor: Theme.accentPrimary
|
||||
padding: 2
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
font.bold: true
|
||||
|
||||
Keys.onDownPressed: root.selectNext()
|
||||
Keys.onUpPressed: root.selectPrev()
|
||||
Keys.onEnterPressed: root.activateSelected()
|
||||
Keys.onReturnPressed: root.activateSelected()
|
||||
Keys.onEscapePressed: appLauncherPanel.hidePanel()
|
||||
}
|
||||
}
|
||||
Behavior on border.color { ColorAnimation { duration: 120 } }
|
||||
Behavior on border.width { NumberAnimation { duration: 120 } }
|
||||
}
|
||||
// App List Card
|
||||
Rectangle {
|
||||
color: Theme.surface
|
||||
radius: 20
|
||||
//border.color: Theme.outline
|
||||
//border.width: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
anchors.margins: 0
|
||||
property int innerPadding: 16
|
||||
// Add an Item for padding
|
||||
Item {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: parent.innerPadding
|
||||
visible: false // for layout only
|
||||
}
|
||||
ListView {
|
||||
id: appList
|
||||
anchors.fill: parent
|
||||
anchors.margins: parent.innerPadding
|
||||
spacing: 2
|
||||
model: root.filteredApps
|
||||
currentIndex: root.selectedIndex
|
||||
delegate: Item {
|
||||
id: appDelegate
|
||||
width: appList.width
|
||||
height: 48
|
||||
property bool hovered: mouseArea.containsMouse
|
||||
property bool isSelected: index === root.selectedIndex
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: hovered || isSelected ? Theme.accentPrimary : "transparent"
|
||||
radius: 12
|
||||
border.color: hovered || isSelected ? Theme.accentPrimary : "transparent"
|
||||
border.width: hovered || isSelected ? 2 : 0
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 120 } }
|
||||
Behavior on border.width { NumberAnimation { duration: 120 } }
|
||||
}
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 10
|
||||
Item {
|
||||
width: 28; height: 28
|
||||
property bool iconLoaded: !modelData.isCalculator && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error
|
||||
Image {
|
||||
id: iconImg
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
cache: false
|
||||
asynchronous: true
|
||||
source: modelData.isCalculator ? "qrc:/icons/calculate.svg" : Quickshell.iconPath(modelData.icon, "")
|
||||
visible: modelData.isCalculator || parent.iconLoaded
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !modelData.isCalculator && !parent.iconLoaded
|
||||
text: "broken_image"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 22
|
||||
color: Theme.accentPrimary
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 1
|
||||
Text {
|
||||
text: modelData.name
|
||||
color: hovered || isSelected ? Theme.onAccent : Theme.textPrimary
|
||||
font.pixelSize: 14
|
||||
font.bold: hovered || isSelected
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : (modelData.comment || modelData.genericName || "")
|
||||
color: hovered || isSelected ? Theme.onAccent : Theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
text: modelData.isCalculator ? "content_copy" : "chevron_right"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 16
|
||||
color: hovered || isSelected ? Theme.onAccent : Theme.textSecondary
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: ripple
|
||||
anchors.fill: parent
|
||||
color: Theme.onAccent
|
||||
opacity: 0.0
|
||||
}
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
ripple.opacity = 0.18
|
||||
rippleNumberAnimation.start()
|
||||
root.selectedIndex = index // update selection on click
|
||||
root.activateSelected()
|
||||
}
|
||||
onPressed: ripple.opacity = 0.18
|
||||
onReleased: ripple.opacity = 0.0
|
||||
}
|
||||
NumberAnimation {
|
||||
id: rippleNumberAnimation
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
to: 0.0
|
||||
duration: 320
|
||||
}
|
||||
// Divider (except last item)
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: index === appList.count - 1 ? 0 : 0.10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Corners {
|
||||
id: launcherCornerRight
|
||||
position: "bottomleft"
|
||||
size: 1.1
|
||||
fillColor: Theme.backgroundPrimary
|
||||
anchors.top: root.top
|
||||
offsetX: 397
|
||||
offsetY: 0
|
||||
}
|
||||
|
||||
Corners {
|
||||
id: launcherCornerLeft
|
||||
position: "bottomright"
|
||||
size: 1.1
|
||||
fillColor: Theme.backgroundPrimary
|
||||
anchors.top: root.top
|
||||
offsetX: -397
|
||||
offsetY: 0
|
||||
}
|
||||
}
|
||||
221
Bar/Modules/Bluetooth.qml
Normal file
221
Bar/Modules/Bluetooth.qml
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import qs.Settings
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: bluetoothDisplay
|
||||
width: 22
|
||||
height: 22
|
||||
|
||||
property color hoverColor: Theme.rippleEffect
|
||||
property real hoverOpacity: 0.0
|
||||
property bool isActive: mouseArea.containsMouse || (bluetoothPopup && bluetoothPopup.visible)
|
||||
|
||||
// Show the Bluetooth popup when clicked
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (bluetoothPopup.visible) {
|
||||
bluetoothPopup.hidePopup();
|
||||
} else {
|
||||
bluetoothPopup.showAt(this, 0, parent.height);
|
||||
}
|
||||
}
|
||||
onEntered: bluetoothDisplay.hoverOpacity = 0.18
|
||||
onExited: bluetoothDisplay.hoverOpacity = 0.0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -4 // Make hover area larger than icon
|
||||
color: hoverColor
|
||||
opacity: isActive ? 0.18 : hoverOpacity
|
||||
radius: height / 2
|
||||
z: 0
|
||||
visible: opacity > 0.01
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "bluetooth"
|
||||
font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
||||
font.pixelSize: 18
|
||||
color: bluetoothPopup.visible ? Theme.accentPrimary : Theme.textPrimary
|
||||
z: 1
|
||||
}
|
||||
|
||||
Behavior on hoverOpacity {
|
||||
NumberAnimation {
|
||||
duration: 120
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// The popup window for device list
|
||||
PopupWindow {
|
||||
id: bluetoothPopup
|
||||
implicitWidth: 350
|
||||
//property int deviceCount: (typeof Bluetooth.devices.count === 'number' && Bluetooth.devices.count >= 0) ? Bluetooth.devices.count : 0
|
||||
//implicitHeight: Math.max(100, Math.min(420, 56 + (deviceCount * 36) + 24))
|
||||
implicitHeight: 400
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property var anchorItem: null
|
||||
property real anchorX
|
||||
property real anchorY
|
||||
|
||||
anchor.item: anchorItem ? anchorItem : null
|
||||
anchor.rect.x: anchorX - (implicitWidth / 2) + (anchorItem ? anchorItem.width / 2 : 0)
|
||||
anchor.rect.y: anchorY + 8 // Move popup further down
|
||||
|
||||
function showAt(item, x, y) {
|
||||
if (!item) {
|
||||
console.warn("Bluetooth: anchorItem is undefined, not showing popup.")
|
||||
return
|
||||
}
|
||||
anchorItem = item
|
||||
anchorX = x
|
||||
anchorY = y
|
||||
visible = true
|
||||
forceActiveFocus()
|
||||
}
|
||||
|
||||
function hidePopup() {
|
||||
visible = false
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
Keys.onEscapePressed: bluetoothPopup.hidePopup()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
color: Theme.backgroundPrimary
|
||||
radius: 12
|
||||
border.width: 1
|
||||
border.color: Theme.surfaceVariant
|
||||
z: 0
|
||||
}
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
id: header
|
||||
width: parent.width
|
||||
height: 56
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
text: "Bluetooth"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: Theme.textPrimary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
}
|
||||
}
|
||||
|
||||
// Device list container with proper margins
|
||||
Rectangle {
|
||||
id: listContainer
|
||||
anchors.top: header.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 8
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: deviceListView
|
||||
anchors.fill: parent
|
||||
spacing: 4
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: Bluetooth.devices
|
||||
delegate: Rectangle {
|
||||
width: parent.width
|
||||
height: 42
|
||||
color: "transparent"
|
||||
radius: 8
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 8
|
||||
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.18)
|
||||
: (deviceMouseArea.containsMouse ? Theme.highlight : "transparent")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: modelData.connected ? "bluetooth" : "bluetooth_disabled"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 20
|
||||
color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: modelData.name || "Unknown Device"
|
||||
color: modelData.connected ? Theme.accentPrimary : Theme.textPrimary
|
||||
font.pixelSize: 14
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.address
|
||||
color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (modelData.connected) {
|
||||
modelData.disconnect()
|
||||
} else {
|
||||
modelData.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll indicator when needed
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 2
|
||||
anchors.top: listContainer.top
|
||||
anchors.bottom: listContainer.bottom
|
||||
width: 4
|
||||
radius: 2
|
||||
color: Theme.textSecondary
|
||||
opacity: deviceListView.contentHeight > deviceListView.height ? 0.3 : 0
|
||||
visible: opacity > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Bar/Modules/Brightness.qml
Normal file
50
Bar/Modules/Brightness.qml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Settings
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: brightnessDisplay
|
||||
property int brightness: -1
|
||||
|
||||
width: pill.width
|
||||
height: pill.height
|
||||
|
||||
FileView {
|
||||
id: brightnessFile
|
||||
path: "/tmp/brightness_osd_level"
|
||||
watchChanges: true
|
||||
blockLoading: true
|
||||
|
||||
onLoaded: updateBrightness()
|
||||
onFileChanged: {
|
||||
brightnessFile.reload()
|
||||
updateBrightness()
|
||||
}
|
||||
|
||||
function updateBrightness() {
|
||||
const val = parseInt(brightnessFile.text())
|
||||
if (!isNaN(val) && val !== brightnessDisplay.brightness) {
|
||||
brightnessDisplay.brightness = val
|
||||
pill.text = brightness + "%"
|
||||
pill.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PillIndicator {
|
||||
id: pill
|
||||
icon: "brightness_high"
|
||||
text: brightness >= 0 ? brightness + "%" : ""
|
||||
pillColor: Theme.surfaceVariant
|
||||
iconCircleColor: Theme.accentPrimary
|
||||
iconTextColor: Theme.backgroundPrimary
|
||||
textColor: Theme.textPrimary
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (brightness >= 0) {
|
||||
pill.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Bar/Modules/ClockWidget.qml
Normal file
18
Bar/Modules/ClockWidget.qml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import QtQuick
|
||||
import qs.Settings
|
||||
|
||||
Rectangle {
|
||||
width: textItem.paintedWidth
|
||||
height: textItem.paintedHeight
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
id: textItem
|
||||
text: Time.time
|
||||
font.family: "Roboto"
|
||||
font.weight: Font.Bold
|
||||
font.pixelSize: 14
|
||||
color: Theme.textPrimary
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
137
Bar/Modules/CustomTrayMenu.qml
Normal file
137
Bar/Modules/CustomTrayMenu.qml
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Settings
|
||||
|
||||
PopupWindow {
|
||||
id: trayMenu
|
||||
implicitWidth: 180
|
||||
implicitHeight: Math.max(40, listView.contentHeight + 12)
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property QsMenuHandle menu
|
||||
property var anchorItem: null
|
||||
property real anchorX
|
||||
property real anchorY
|
||||
|
||||
anchor.item: anchorItem ? anchorItem : null
|
||||
anchor.rect.x: anchorX
|
||||
anchor.rect.y: anchorY
|
||||
|
||||
function showAt(item, x, y) {
|
||||
if (!item) {
|
||||
console.warn("CustomTrayMenu: anchorItem is undefined, not showing menu.");
|
||||
return;
|
||||
}
|
||||
anchorItem = item
|
||||
anchorX = x
|
||||
anchorY = y
|
||||
visible = true
|
||||
forceActiveFocus()
|
||||
Qt.callLater(() => trayMenu.anchor.updateAnchor())
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
visible = false
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
Keys.onEscapePressed: trayMenu.hideMenu()
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: trayMenu.menu
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
color: Theme.backgroundPrimary
|
||||
border.color: Theme.accentPrimary
|
||||
border.width: 2
|
||||
radius: 20
|
||||
z: 0
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
spacing: 2
|
||||
interactive: false
|
||||
enabled: trayMenu.visible
|
||||
clip: true
|
||||
|
||||
model: ScriptModel {
|
||||
values: opener.children ? [...opener.children.values] : []
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
required property var modelData
|
||||
|
||||
width: listView.width
|
||||
height: (modelData?.isSeparator) ? 8 : 28
|
||||
color: "transparent"
|
||||
radius: 6
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 20
|
||||
height: 1
|
||||
color: Qt.darker(Theme.backgroundPrimary, 1.4)
|
||||
visible: modelData?.isSeparator ?? false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Theme.highlight : "transparent"
|
||||
radius: 6
|
||||
visible: !(modelData?.isSeparator ?? false)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled ?? true) ? Theme.textPrimary : Theme.textDisabled
|
||||
text: modelData?.text ?? ""
|
||||
font.pixelSize: 13
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 16
|
||||
Layout.preferredHeight: 16
|
||||
source: modelData?.icon ?? ""
|
||||
visible: (modelData?.icon ?? "") !== ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible
|
||||
|
||||
onClicked: {
|
||||
if (modelData && !modelData.isSeparator) {
|
||||
modelData.triggered()
|
||||
trayMenu.hideMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
Bar/Modules/SystemTray.qml
Normal file
128
Bar/Modules/SystemTray.qml
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
|
||||
Row {
|
||||
property var bar
|
||||
property var shell
|
||||
property var trayMenu
|
||||
spacing: 8
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
property bool containsMouse: false
|
||||
property var systemTray: SystemTray
|
||||
|
||||
Repeater {
|
||||
model: systemTray.items
|
||||
delegate: Item {
|
||||
width: 24
|
||||
height: 24
|
||||
// Hide Spotify icon, or adjust to your liking
|
||||
visible: modelData && modelData.id !== "spotify"
|
||||
property bool isHovered: trayMouseArea.containsMouse
|
||||
|
||||
// Hover scale animation
|
||||
scale: isHovered ? 1.15 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle rotation on hover
|
||||
rotation: isHovered ? 5 : 0
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: trayIcon
|
||||
anchors.centerIn: parent
|
||||
width: 18
|
||||
height: 18
|
||||
sourceSize.width: 18
|
||||
sourceSize.height: 18
|
||||
smooth: false // Memory savings
|
||||
asynchronous: true
|
||||
cache: false
|
||||
source: {
|
||||
let icon = modelData?.icon || "";
|
||||
if (!icon) return "";
|
||||
// Process icon path
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||
return `file://${path}/${fileName}`;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
// console.log("Tray icon for", modelData?.id, ":", modelData?.icon)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: (mouse) => {
|
||||
if (!modelData) return;
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Close any open menu first
|
||||
if (trayMenu && trayMenu.visible) {
|
||||
trayMenu.hideMenu()
|
||||
}
|
||||
|
||||
if (!modelData.onlyMenu) {
|
||||
modelData.activate()
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
// Close any open menu first
|
||||
if (trayMenu && trayMenu.visible) {
|
||||
trayMenu.hideMenu()
|
||||
}
|
||||
|
||||
modelData.secondaryActivate && modelData.secondaryActivate()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
console.log("Right click on", modelData.id, "hasMenu:", modelData.hasMenu, "menu:", modelData.menu)
|
||||
|
||||
// If menu is already visible, close it
|
||||
if (trayMenu && trayMenu.visible) {
|
||||
trayMenu.hideMenu()
|
||||
return
|
||||
}
|
||||
|
||||
if (modelData.hasMenu && modelData.menu && trayMenu) {
|
||||
// Anchor the menu to the tray icon item (parent) and position it below the icon
|
||||
const menuX = (width / 2) - (trayMenu.width / 2);
|
||||
const menuY = height + 20;
|
||||
trayMenu.menu = modelData.menu;
|
||||
trayMenu.showAt(parent, menuX, menuY);
|
||||
} else {
|
||||
// console.log("No menu available for", modelData.id, "or trayMenu not set")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
// No cache cleanup needed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Bar/Modules/Time.qml
Normal file
16
Bar/Modules/Time.qml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property string time: {
|
||||
Qt.formatDateTime(clock.date, "hh:mm")
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Seconds
|
||||
}
|
||||
}
|
||||
44
Bar/Modules/Volume.qml
Normal file
44
Bar/Modules/Volume.qml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Settings
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: volumeDisplay
|
||||
property var shell
|
||||
property int volume: 0
|
||||
|
||||
// The total width will match the pill's width
|
||||
width: pillIndicator.width
|
||||
height: pillIndicator.height
|
||||
|
||||
PillIndicator {
|
||||
id: pillIndicator
|
||||
icon: volume === 0 ? "volume_off" : (volume < 30 ? "volume_down" : "volume_up")
|
||||
text: volume + "%"
|
||||
|
||||
pillColor: Theme.surfaceVariant
|
||||
iconCircleColor: Theme.accentPrimary
|
||||
iconTextColor: Theme.backgroundPrimary
|
||||
textColor: Theme.textPrimary
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shell ?? null
|
||||
function onVolumeChanged() {
|
||||
if (shell && shell.volume !== volume) {
|
||||
volume = shell.volume
|
||||
pillIndicator.text = volume + "%"
|
||||
pillIndicator.icon = volume === 0 ? "volume_off" : (volume < 30 ? "volume_down" : "volume_up")
|
||||
pillIndicator.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (shell && shell.volume !== undefined) {
|
||||
volume = shell.volume
|
||||
pillIndicator.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
430
Bar/Modules/Wifi.qml
Normal file
430
Bar/Modules/Wifi.qml
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.Settings
|
||||
|
||||
Item {
|
||||
id: wifiDisplay
|
||||
width: 22
|
||||
height: 22
|
||||
|
||||
property color hoverColor: Theme.rippleEffect
|
||||
property real hoverOpacity: 0.0
|
||||
property bool isActive: mouseArea.containsMouse || (wifiPanel && wifiPanel.visible)
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (wifiPanel.visible) {
|
||||
wifiPanel.hidePopup();
|
||||
} else {
|
||||
wifiPanel.showAt(this, 0, parent.height);
|
||||
}
|
||||
}
|
||||
onEntered: wifiDisplay.hoverOpacity = 0.18
|
||||
onExited: wifiDisplay.hoverOpacity = 0.0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -4
|
||||
color: hoverColor
|
||||
opacity: isActive ? 0.18 : hoverOpacity
|
||||
radius: height / 2
|
||||
z: 0
|
||||
visible: opacity > 0.01
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "wifi"
|
||||
font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined"
|
||||
font.pixelSize: 18
|
||||
color: wifiPanel.visible ? Theme.accentPrimary : Theme.textPrimary
|
||||
z: 1
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: wifiPanel
|
||||
implicitWidth: 350
|
||||
implicitHeight: 400
|
||||
color: "transparent"
|
||||
visible: false
|
||||
property var anchorItem: null
|
||||
property real anchorX
|
||||
property real anchorY
|
||||
property var networks: [] // { ssid, signal, security, connected }
|
||||
property string connectingSsid: ""
|
||||
property string errorMsg: ""
|
||||
property string passwordPromptSsid: ""
|
||||
property string passwordInput: ""
|
||||
property bool showPasswordPrompt: false
|
||||
|
||||
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
|
||||
function showAt(item, x, y) {
|
||||
if (!item) {
|
||||
console.warn("Wifi: anchorItem is undefined, not showing panel.");
|
||||
return;
|
||||
}
|
||||
anchorItem = item
|
||||
anchorX = x
|
||||
anchorY = y
|
||||
visible = true
|
||||
refreshNetworks()
|
||||
Qt.callLater(() => {
|
||||
wifiPanel.x = anchorX - (wifiPanel.width / 2) + (anchorItem ? anchorItem.width / 2 : 0)
|
||||
wifiPanel.y = anchorY + 8
|
||||
})
|
||||
}
|
||||
function hidePopup() {
|
||||
visible = false
|
||||
showPasswordPrompt = false
|
||||
errorMsg = ""
|
||||
}
|
||||
|
||||
// Scan for networks
|
||||
Process {
|
||||
id: scanProcess
|
||||
running: false
|
||||
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var lines = text.split("\n");
|
||||
var nets = [];
|
||||
var seen = {};
|
||||
for (var i = 0; i < lines.length; ++i) {
|
||||
var line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
var parts = line.split(":");
|
||||
var ssid = parts[0];
|
||||
var security = parts[1];
|
||||
var signal = parseInt(parts[2]);
|
||||
var inUse = parts[3] === "*";
|
||||
if (ssid && !seen[ssid]) {
|
||||
nets.push({ ssid: ssid, security: security, signal: signal, connected: inUse });
|
||||
seen[ssid] = true;
|
||||
}
|
||||
}
|
||||
wifiPanel.networks = nets;
|
||||
}
|
||||
}
|
||||
}
|
||||
function refreshNetworks() {
|
||||
scanProcess.running = true;
|
||||
}
|
||||
|
||||
// Connect to a network
|
||||
Process {
|
||||
id: connectProcess
|
||||
property string ssid: ""
|
||||
property string password: ""
|
||||
running: false
|
||||
command: password ? ["nmcli", "device", "wifi", "connect", ssid, "password", password] : ["nmcli", "device", "wifi", "connect", ssid]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
wifiPanel.connectingSsid = "";
|
||||
wifiPanel.showPasswordPrompt = false;
|
||||
wifiPanel.errorMsg = "";
|
||||
refreshNetworks();
|
||||
}
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
wifiPanel.connectingSsid = "";
|
||||
wifiPanel.errorMsg = text;
|
||||
wifiPanel.showPasswordPrompt = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
function connectNetwork(ssid, security) {
|
||||
if (security && security !== "--") {
|
||||
// Prompt for password
|
||||
passwordPromptSsid = ssid;
|
||||
passwordInput = "";
|
||||
showPasswordPrompt = true;
|
||||
} else {
|
||||
connectingSsid = ssid;
|
||||
connectProcess.ssid = ssid;
|
||||
connectProcess.password = "";
|
||||
connectProcess.running = true;
|
||||
}
|
||||
}
|
||||
function submitPassword() {
|
||||
connectingSsid = passwordPromptSsid;
|
||||
connectProcess.ssid = passwordPromptSsid;
|
||||
connectProcess.password = passwordInput;
|
||||
connectProcess.running = true;
|
||||
}
|
||||
// Disconnect
|
||||
Process {
|
||||
id: disconnectProcess
|
||||
property string ssid: ""
|
||||
running: false
|
||||
command: ["nmcli", "connection", "down", "id", ssid]
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
refreshNetworks();
|
||||
}
|
||||
}
|
||||
}
|
||||
function disconnectNetwork(ssid) {
|
||||
disconnectProcess.ssid = ssid;
|
||||
disconnectProcess.running = true;
|
||||
}
|
||||
|
||||
// UI
|
||||
Rectangle {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
radius: 12
|
||||
border.width: 1
|
||||
border.color: Theme.surfaceVariant
|
||||
color: Theme.backgroundPrimary
|
||||
z: 0
|
||||
}
|
||||
// Header
|
||||
Rectangle {
|
||||
id: header
|
||||
width: parent.width
|
||||
height: 56
|
||||
color: "transparent"
|
||||
Text {
|
||||
text: "Wi-Fi"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: Theme.textPrimary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
}
|
||||
// Refresh button
|
||||
Rectangle {
|
||||
id: refreshBtn
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
color: Theme.surfaceVariant
|
||||
border.color: refreshMouseArea.containsMouse ? Theme.accentPrimary : Theme.outline
|
||||
border.width: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
MouseArea {
|
||||
id: refreshMouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: wifiPanel.refreshNetworks()
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "refresh"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 20
|
||||
color: Theme.accentPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
// Error message
|
||||
Text {
|
||||
visible: wifiPanel.errorMsg.length > 0
|
||||
text: wifiPanel.errorMsg
|
||||
color: Theme.error
|
||||
font.pixelSize: 12
|
||||
anchors.top: header.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
anchors.topMargin: 2
|
||||
}
|
||||
// Device list container
|
||||
Rectangle {
|
||||
id: listContainer
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: wifiPanel.showPasswordPrompt ? 68 : 0
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 8
|
||||
color: "transparent"
|
||||
clip: true
|
||||
ListView {
|
||||
id: networkListView
|
||||
anchors.fill: parent
|
||||
spacing: 4
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: wifiPanel.networks
|
||||
delegate: Rectangle {
|
||||
id: networkEntry
|
||||
width: parent.width
|
||||
height: modelData.ssid === wifiPanel.passwordPromptSsid && wifiPanel.showPasswordPrompt ? 110 : 42
|
||||
color: "transparent"
|
||||
radius: 8
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 8
|
||||
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.18)
|
||||
: ((networkMouseArea.containsMouse || (modelData.ssid === wifiPanel.passwordPromptSsid && wifiPanel.showPasswordPrompt)) ? Theme.highlight : "transparent")
|
||||
z: 0
|
||||
}
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 0
|
||||
anchors.bottomMargin: 0
|
||||
anchors.topMargin: 0
|
||||
|
||||
height: 42
|
||||
spacing: 12
|
||||
// Signal icon
|
||||
Text {
|
||||
text: signalIcon(modelData.signal)
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 20
|
||||
color: hovered ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
Text {
|
||||
text: modelData.ssid || "Unknown Network"
|
||||
color: hovered ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary)
|
||||
font.pixelSize: 14
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: modelData.security && modelData.security !== "--" ? modelData.security : "Open"
|
||||
color: hovered ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
// Status
|
||||
Text {
|
||||
visible: modelData.connected
|
||||
text: "connected"
|
||||
color: hovered ? Theme.backgroundPrimary : Theme.accentPrimary
|
||||
font.pixelSize: 11
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
// Password prompt dropdown (only for selected network)
|
||||
Rectangle {
|
||||
visible: modelData.ssid === wifiPanel.passwordPromptSsid && wifiPanel.showPasswordPrompt
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: 8
|
||||
color: Theme.surfaceVariant
|
||||
border.color: passwordField.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||
border.width: 1
|
||||
anchors.bottom: parent.bottom
|
||||
z: 2
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 10
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
radius: 8
|
||||
color: Theme.surfaceVariant
|
||||
border.color: passwordField.activeFocus ? Theme.accentPrimary : Theme.outline
|
||||
border.width: 1
|
||||
TextInput {
|
||||
id: passwordField
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
text: wifiPanel.passwordInput
|
||||
font.pixelSize: 13
|
||||
color: Theme.textPrimary
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
clip: true
|
||||
focus: true
|
||||
selectByMouse: true
|
||||
activeFocusOnTab: true
|
||||
inputMethodHints: Qt.ImhNone
|
||||
echoMode: TextInput.Password
|
||||
onTextChanged: wifiPanel.passwordInput = text
|
||||
onAccepted: wifiPanel.submitPassword()
|
||||
MouseArea {
|
||||
id: passwordMouseArea
|
||||
anchors.fill: parent
|
||||
onClicked: passwordField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 36
|
||||
radius: 18
|
||||
color: Theme.accentPrimary
|
||||
border.color: Theme.accentPrimary
|
||||
border.width: 0
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: wifiPanel.submitPassword()
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Connect"
|
||||
color: Theme.backgroundPrimary
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: networkMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (modelData.connected) {
|
||||
wifiPanel.disconnectNetwork(modelData.ssid)
|
||||
} else if (modelData.security && modelData.security !== "--") {
|
||||
wifiPanel.passwordPromptSsid = modelData.ssid;
|
||||
wifiPanel.passwordInput = "";
|
||||
wifiPanel.showPasswordPrompt = true;
|
||||
} else {
|
||||
wifiPanel.connectNetwork(modelData.ssid, modelData.security)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Helper for hover text color
|
||||
property bool hovered: networkMouseArea.containsMouse || (modelData.ssid === wifiPanel.passwordPromptSsid && wifiPanel.showPasswordPrompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Scroll indicator
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 2
|
||||
anchors.top: listContainer.top
|
||||
anchors.bottom: listContainer.bottom
|
||||
width: 4
|
||||
radius: 2
|
||||
color: Theme.textSecondary
|
||||
opacity: networkListView.contentHeight > networkListView.height ? 0.3 : 0
|
||||
visible: opacity > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for signal icon
|
||||
function signalIcon(signal) {
|
||||
if (signal >= 80) return "network_wifi_4_bar";
|
||||
if (signal >= 60) return "network_wifi_3_bar";
|
||||
if (signal >= 40) return "network_wifi_2_bar";
|
||||
if (signal >= 20) return "network_wifi_1_bar";
|
||||
return "wifi_0_bar";
|
||||
}
|
||||
}
|
||||
229
Bar/Modules/Workspace.qml
Normal file
229
Bar/Modules/Workspace.qml
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Io
|
||||
import qs.Settings
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ListModel workspaces: ListModel {}
|
||||
property bool isDestroying: false
|
||||
property bool hovered: false
|
||||
|
||||
signal workspaceChanged(int workspaceId, color accentColor)
|
||||
|
||||
property real masterProgress: 0.0
|
||||
property bool effectsActive: false
|
||||
property color effectColor: Theme.accentPrimary
|
||||
|
||||
property int horizontalPadding: 16
|
||||
property int spacingBetweenPills: 8
|
||||
|
||||
width: {
|
||||
let total = 0
|
||||
for (let i = 0; i < workspaces.count; i++) {
|
||||
const ws = workspaces.get(i)
|
||||
if (ws.isFocused) total += 44
|
||||
else if (ws.isActive) total += 28
|
||||
else total += 16
|
||||
}
|
||||
total += Math.max(workspaces.count - 1, 0) * spacingBetweenPills
|
||||
total += horizontalPadding * 2
|
||||
return total
|
||||
}
|
||||
|
||||
height: 36
|
||||
|
||||
Component.onCompleted: updateWorkspaceList()
|
||||
Connections {
|
||||
target: Niri
|
||||
function onWorkspacesChanged() { updateWorkspaceList(); }
|
||||
function onFocusedWorkspaceIndexChanged() { updateWorkspaceFocus(); }
|
||||
}
|
||||
|
||||
function triggerUnifiedWave() {
|
||||
effectColor = Theme.accentPrimary
|
||||
masterAnimation.restart()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: masterAnimation
|
||||
PropertyAction { target: root; property: "effectsActive"; value: true }
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "masterProgress"
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
PropertyAction { target: root; property: "effectsActive"; value: false }
|
||||
PropertyAction { target: root; property: "masterProgress"; value: 0.0 }
|
||||
}
|
||||
|
||||
function updateWorkspaceList() {
|
||||
const newList = Niri.workspaces || []
|
||||
workspaces.clear()
|
||||
for (let i = 0; i < newList.length; i++) {
|
||||
const ws = newList[i]
|
||||
workspaces.append({
|
||||
id: ws.id,
|
||||
idx: ws.idx,
|
||||
name: ws.name || "",
|
||||
output: ws.output,
|
||||
isActive: ws.is_active,
|
||||
isFocused: ws.is_focused,
|
||||
isUrgent: ws.is_urgent
|
||||
})
|
||||
}
|
||||
updateWorkspaceFocus()
|
||||
}
|
||||
|
||||
function updateWorkspaceFocus() {
|
||||
const focusedId = Niri.workspaces?.[Niri.focusedWorkspaceIndex]?.id ?? -1
|
||||
for (let i = 0; i < workspaces.count; i++) {
|
||||
const ws = workspaces.get(i)
|
||||
const isFocused = ws.id === focusedId
|
||||
const isActive = isFocused
|
||||
if (ws.isFocused !== isFocused || ws.isActive !== isActive) {
|
||||
workspaces.setProperty(i, "isFocused", isFocused)
|
||||
workspaces.setProperty(i, "isActive", isActive)
|
||||
if (isFocused) {
|
||||
root.triggerUnifiedWave()
|
||||
root.workspaceChanged(ws.id, Theme.accentPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: workspaceBackground
|
||||
width: parent.width - 15
|
||||
height: 26
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: 12
|
||||
color: Theme.surfaceVariant
|
||||
border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1)
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
layer.effect: DropShadow {
|
||||
color: "black"
|
||||
radius: 12
|
||||
samples: 24
|
||||
verticalOffset: 0
|
||||
horizontalOffset: 0
|
||||
opacity: 0.10
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: pillRow
|
||||
spacing: spacingBetweenPills
|
||||
anchors.verticalCenter: workspaceBackground.verticalCenter
|
||||
width: root.width - horizontalPadding * 2
|
||||
x: horizontalPadding
|
||||
Repeater {
|
||||
model: root.workspaces
|
||||
Rectangle {
|
||||
id: workspacePill
|
||||
height: 12
|
||||
width: {
|
||||
if (model.isFocused) return 44
|
||||
else if (model.isActive) return 28
|
||||
else return 16
|
||||
}
|
||||
radius: {
|
||||
if (model.isFocused) return 12 // half of focused height (if you want to animate this too)
|
||||
else return 6
|
||||
}
|
||||
color: {
|
||||
if (model.isFocused) return Theme.accentPrimary
|
||||
if (model.isActive) return Theme.accentPrimary.lighter(130)
|
||||
if (model.isUrgent) return Theme.error
|
||||
return Qt.lighter(Theme.surfaceVariant, 1.6)
|
||||
}
|
||||
scale: model.isFocused ? 1.0 : 0.9
|
||||
z: 0
|
||||
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
// Burst effect overlay for focused pill (smaller outline)
|
||||
Rectangle {
|
||||
id: pillBurst
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 18 * root.masterProgress
|
||||
height: parent.height + 18 * root.masterProgress
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: root.effectColor
|
||||
border.width: 2 + 6 * (1.0 - root.masterProgress)
|
||||
opacity: root.effectsActive && model.isFocused
|
||||
? (1.0 - root.masterProgress) * 0.7
|
||||
: 0
|
||||
visible: root.effectsActive && model.isFocused
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea to open/close Applauncher
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (appLauncherPanel && appLauncherPanel.visible) {
|
||||
appLauncherPanel.hidePanel();
|
||||
} else if (appLauncherPanel) {
|
||||
appLauncherPanel.showAt();
|
||||
}
|
||||
}
|
||||
z: 1000 // ensure it's above other content
|
||||
hoverEnabled: true
|
||||
onEntered: root.hovered = true
|
||||
onExited: root.hovered = false
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
root.isDestroying = true
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue