Bugfix: PanelWithOverlay would close when clicking in the background of the panel.

This commit is contained in:
quadbyte 2025-08-06 20:41:39 -04:00
parent 0b5f1cd9e5
commit 8c7f6e491d
6 changed files with 543 additions and 314 deletions

View file

@ -7,13 +7,67 @@ import qs.Settings
PanelWithOverlay {
id: ioSelector
signal panelClosed()
property int tabIndex: 0
property Item anchorItem: null
signal panelClosed()
function sinkNodes() {
let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {
return n.isSink && n.audio && n.isStream === false;
}) : [];
if (Pipewire.defaultAudioSink)
nodes = nodes.slice().sort(function(a, b) {
if (a.id === Pipewire.defaultAudioSink.id)
return -1;
if (b.id === Pipewire.defaultAudioSink.id)
return 1;
return 0;
});
return nodes;
}
function sourceNodes() {
let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {
return !n.isSink && n.audio && n.isStream === false;
}) : [];
if (Pipewire.defaultAudioSource)
nodes = nodes.slice().sort(function(a, b) {
if (a.id === Pipewire.defaultAudioSource.id)
return -1;
if (b.id === Pipewire.defaultAudioSource.id)
return 1;
return 0;
});
return nodes;
}
Component.onCompleted: {
if (Pipewire.nodes && Pipewire.nodes.values) {
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
var n = Pipewire.nodes.values[i];
}
}
}
Component.onDestruction: {
}
onVisibleChanged: {
if (!visible)
panelClosed();
}
// Bind all Pipewire nodes so their properties are valid
PwObjectTracker {
id: nodeTracker
objects: Pipewire.nodes
}
@ -27,6 +81,11 @@ PanelWithOverlay {
anchors.topMargin: 4
anchors.rightMargin: 4
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
@ -40,45 +99,59 @@ PanelWithOverlay {
Tabs {
id: ioTabs
tabsModel: [
{ label: "Output", icon: "volume_up" },
{ label: "Input", icon: "mic" }
]
tabsModel: [{
"label": "Output",
"icon": "volume_up"
}, {
"label": "Input",
"icon": "mic"
}]
currentIndex: tabIndex
onTabChanged: {
tabIndex = currentIndex;
}
}
}
// Add vertical space between tabs and entries
Item { height: 36; Layout.fillWidth: true }
Item {
height: 36
Layout.fillWidth: true
}
// Output Devices
Flickable {
id: sinkList
visible: tabIndex === 0
contentHeight: sinkColumn.height
clip: true
interactive: contentHeight > height
width: parent.width
height: 220
ScrollBar.vertical: ScrollBar {}
ColumnLayout {
id: sinkColumn
width: sinkList.width
spacing: 6
Repeater {
model: ioSelector.sinkNodes()
Rectangle {
width: parent.width
height: 36
color: "transparent"
radius: 6
RowLayout {
anchors.fill: parent
anchors.margins: 6
spacing: 8
Text {
text: "volume_up"
font.family: "Material Symbols Outlined"
@ -86,10 +159,12 @@ PanelWithOverlay {
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button
Text {
text: modelData.nickname || modelData.description || modelData.name
font.bold: true
@ -99,6 +174,7 @@ PanelWithOverlay {
maximumLineCount: 1
Layout.fillWidth: true
}
Text {
text: modelData.description !== modelData.nickname ? modelData.description : ""
font.pixelSize: 10
@ -107,15 +183,19 @@ PanelWithOverlay {
maximumLineCount: 1
Layout.fillWidth: true
}
}
Rectangle {
visible: Pipewire.preferredDefaultAudioSink !== modelData
width: 60; height: 20
width: 60
height: 20
radius: 4
color: Theme.accentPrimary
border.color: Theme.accentPrimary
border.width: 1
Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
text: "Set"
@ -123,12 +203,15 @@ PanelWithOverlay {
font.pixelSize: 10
font.bold: true
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Pipewire.preferredDefaultAudioSink = modelData
}
}
Text {
text: "(Current)"
visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id
@ -136,37 +219,51 @@ PanelWithOverlay {
font.pixelSize: 10
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
ScrollBar.vertical: ScrollBar {
}
}
// Input Devices
Flickable {
id: sourceList
visible: tabIndex === 1
contentHeight: sourceColumn.height
clip: true
interactive: contentHeight > height
width: parent.width
height: 220
ScrollBar.vertical: ScrollBar {}
ColumnLayout {
id: sourceColumn
width: sourceList.width
spacing: 6
Repeater {
model: ioSelector.sourceNodes()
Rectangle {
width: parent.width
height: 36
color: "transparent"
radius: 6
RowLayout {
anchors.fill: parent
anchors.margins: 6
spacing: 8
Text {
text: "mic"
font.family: "Material Symbols Outlined"
@ -174,10 +271,12 @@ PanelWithOverlay {
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button
Text {
text: modelData.nickname || modelData.description || modelData.name
font.bold: true
@ -187,6 +286,7 @@ PanelWithOverlay {
maximumLineCount: 1
Layout.fillWidth: true
}
Text {
text: modelData.description !== modelData.nickname ? modelData.description : ""
font.pixelSize: 10
@ -195,15 +295,19 @@ PanelWithOverlay {
maximumLineCount: 1
Layout.fillWidth: true
}
}
Rectangle {
visible: Pipewire.preferredDefaultAudioSource !== modelData
width: 60; height: 20
width: 60
height: 20
radius: 4
color: Theme.accentPrimary
border.color: Theme.accentPrimary
border.width: 1
Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
text: "Set"
@ -211,12 +315,15 @@ PanelWithOverlay {
font.pixelSize: 10
font.bold: true
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Pipewire.preferredDefaultAudioSource = modelData
}
}
Text {
text: "(Current)"
visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id
@ -224,55 +331,25 @@ PanelWithOverlay {
font.pixelSize: 10
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
}
}
}
function sinkNodes() {
let nodes = Pipewire.nodes && Pipewire.nodes.values
? Pipewire.nodes.values.filter(function(n) {
return n.isSink && n.audio && n.isStream === false;
})
: [];
if (Pipewire.defaultAudioSink) {
nodes = nodes.slice().sort(function(a, b) {
if (a.id === Pipewire.defaultAudioSink.id) return -1;
if (b.id === Pipewire.defaultAudioSink.id) return 1;
return 0;
});
}
return nodes;
}
function sourceNodes() {
let nodes = Pipewire.nodes && Pipewire.nodes.values
? Pipewire.nodes.values.filter(function(n) {
return !n.isSink && n.audio && n.isStream === false;
})
: [];
if (Pipewire.defaultAudioSource) {
nodes = nodes.slice().sort(function(a, b) {
if (a.id === Pipewire.defaultAudioSource.id) return -1;
if (b.id === Pipewire.defaultAudioSource.id) return 1;
return 0;
});
}
return nodes;
}
ScrollBar.vertical: ScrollBar {
}
Component.onCompleted: {
if (Pipewire.nodes && Pipewire.nodes.values) {
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
var n = Pipewire.nodes.values[i];
}
}
}
Connections {
target: Pipewire
function onReadyChanged() {
if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) {
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
@ -280,15 +357,14 @@ PanelWithOverlay {
}
}
}
function onDefaultAudioSinkChanged() {
}
function onDefaultAudioSourceChanged() {
}
target: Pipewire
}
Component.onDestruction: {
}
onVisibleChanged: {
if (!visible) panelClosed();
}
}

View file

@ -1,11 +1,11 @@
import "../../Helpers/Holidays.js" as Holidays
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Components
import qs.Settings
import Quickshell.Wayland
import "../../Helpers/Holidays.js" as Holidays
PanelWithOverlay {
id: calendarOverlay
@ -22,6 +22,11 @@ PanelWithOverlay {
anchors.topMargin: 4
anchors.rightMargin: 4
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
@ -60,13 +65,15 @@ PanelWithOverlay {
calendar.month = newDate.getMonth();
}
}
}
DayOfWeekRow {
Layout.fillWidth: true
spacing: 0
Layout.leftMargin: 8 // Align with grid
Layout.leftMargin: 8 // Align with grid
Layout.rightMargin: 8
delegate: Text {
text: shortName
color: Theme.textPrimary
@ -77,16 +84,11 @@ PanelWithOverlay {
horizontalAlignment: Text.AlignHCenter
width: 32
}
}
MonthGrid {
id: calendar
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 0
month: Time.date.getMonth()
year: Time.date.getFullYear()
property var holidays: []
@ -96,12 +98,19 @@ PanelWithOverlay {
calendar.holidays = holidays;
});
}
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 0
month: Time.date.getMonth()
year: Time.date.getFullYear()
onMonthChanged: updateHolidays()
onYearChanged: updateHolidays()
Component.onCompleted: updateHolidays()
// Optionally, update when the panel becomes visible
Connections {
target: calendarOverlay
function onVisibleChanged() {
if (calendarOverlay.visible) {
calendar.month = Time.date.getMonth();
@ -109,29 +118,35 @@ PanelWithOverlay {
calendar.updateHolidays();
}
}
target: calendarOverlay
}
delegate: Rectangle {
width: 32
height: 32
radius: 8
property var holidayInfo: calendar.holidays.filter(function(h) {
var d = new Date(h.date);
return d.getDate() === model.day && d.getMonth() === model.month && d.getFullYear() === model.year;
})
property bool isHoliday: holidayInfo.length > 0
width: 32
height: 32
radius: 8
color: {
if (model.today)
return Theme.accentPrimary;
if (mouseArea2.containsMouse)
return Theme.backgroundTertiary;
return "transparent";
}
// Holiday dot indicator
Rectangle {
visible: isHoliday
width: 4; height: 4
width: 4
height: 4
radius: 4
color: Theme.accentTertiary
anchors.top: parent.top
@ -145,7 +160,7 @@ PanelWithOverlay {
anchors.centerIn: parent
text: model.day
color: model.today ? Theme.onAccent : Theme.textPrimary
opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1.0 : 0.7) : 0.3
opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1 : 0.7) : 0.3
font.pixelSize: 13
font.family: Theme.fontFamily
font.bold: model.today ? true : false
@ -153,6 +168,7 @@ PanelWithOverlay {
MouseArea {
id: mouseArea2
anchors.fill: parent
hoverEnabled: true
onEntered: {
@ -167,21 +183,28 @@ PanelWithOverlay {
onExited: holidayTooltip.tooltipVisible = false
}
Behavior on color {
ColorAnimation {
duration: 150
}
}
StyledTooltip {
id: holidayTooltip
text: ""
tooltipVisible: false
targetItem: null
delay: 100
}
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
}
}
}
}

View file

@ -1,61 +1,31 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Settings
import QtQuick.Layouts
import qs.Components
import qs.Settings
PanelWithOverlay {
id: notificationHistoryWin
property string historyFilePath: Settings.settingsDir + "notification_history.json"
property bool hasUnread: notificationHistoryWinRect.hasUnread && !notificationHistoryWinRect.visible
function addToHistory(notification) { notificationHistoryWinRect.addToHistory(notification) }
function addToHistory(notification) {
notificationHistoryWinRect.addToHistory(notification);
}
Rectangle {
id: notificationHistoryWinRect
implicitWidth: 400
property int maxPopupHeight: 800
property int minPopupHeight: 210
property int contentHeight: headerRow.height + historyList.contentHeight + 56
implicitHeight: Math.max(Math.min(contentHeight, maxPopupHeight), minPopupHeight)
visible: parent.visible
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 4
anchors.rightMargin: 4
color: Theme.backgroundPrimary
radius: 20
property int maxHistory: 100
property bool hasUnread: false
signal unreadChanged(bool hasUnread)
ListModel {
id: historyModel
}
FileView {
id: historyFileView
path: notificationHistoryWin.historyFilePath
blockLoading: true
printErrors: true
watchChanges: true
JsonAdapter {
id: historyAdapter
property var notifications: []
}
onFileChanged: historyFileView.reload()
onLoaded: notificationHistoryWinRect.loadHistory()
onLoadFailed: function (error) {
historyAdapter.notifications = [];
historyFileView.writeAdapter();
}
Component.onCompleted: if (path)
reload()
}
function updateHasUnread() {
var unread = false;
for (let i = 0; i < historyModel.count; ++i) {
@ -80,9 +50,11 @@ PanelWithOverlay {
if (typeof n === 'object' && n !== null) {
if (n.read === undefined)
n.read = false;
// Mark as read if window is open
if (notificationHistoryWinRect.visible)
n.read = true;
historyModel.append(n);
}
}
@ -95,19 +67,19 @@ PanelWithOverlay {
const count = Math.min(historyModel.count, maxHistory);
for (let i = 0; i < count; ++i) {
let obj = historyModel.get(i);
if (typeof obj === 'object' && obj !== null) {
if (typeof obj === 'object' && obj !== null)
historyArray.push({
id: obj.id,
appName: obj.appName,
summary: obj.summary,
body: obj.body,
timestamp: obj.timestamp,
read: obj.read === undefined ? false : obj.read
"id": obj.id,
"appName": obj.appName,
"summary": obj.summary,
"body": obj.body,
"timestamp": obj.timestamp,
"read": obj.read === undefined ? false : obj.read
});
}
}
historyAdapter.notifications = historyArray;
Qt.callLater(function () {
Qt.callLater(function() {
historyFileView.writeAdapter();
});
updateHasUnread();
@ -116,12 +88,12 @@ PanelWithOverlay {
function addToHistory(notification) {
if (!notification.id)
notification.id = Date.now();
if (!notification.timestamp)
notification.timestamp = new Date().toISOString();
// Mark as read if window is open
notification.read = visible;
// Remove duplicate by id
for (let i = 0; i < historyModel.count; ++i) {
if (historyModel.get(i).id === notification.id) {
@ -129,11 +101,10 @@ PanelWithOverlay {
break;
}
}
historyModel.insert(0, notification);
if (historyModel.count > maxHistory)
historyModel.remove(maxHistory);
saveHistory();
}
@ -146,6 +117,7 @@ PanelWithOverlay {
function formatTimestamp(ts) {
if (!ts)
return "";
var date = typeof ts === "number" ? new Date(ts) : new Date(Date.parse(ts));
var y = date.getFullYear();
var m = (date.getMonth() + 1).toString().padStart(2, '0');
@ -155,6 +127,15 @@ PanelWithOverlay {
return `${y}-${m}-${d} ${h}:${min}`;
}
implicitWidth: 400
implicitHeight: Math.max(Math.min(contentHeight, maxPopupHeight), minPopupHeight)
visible: parent.visible
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 4
anchors.rightMargin: 4
color: Theme.backgroundPrimary
radius: 20
onVisibleChanged: {
if (visible) {
// Mark all as read when popup is opened
@ -167,9 +148,46 @@ PanelWithOverlay {
}
if (changed)
saveHistory();
}
}
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ListModel {
id: historyModel
}
FileView {
id: historyFileView
path: notificationHistoryWin.historyFilePath
blockLoading: true
printErrors: true
watchChanges: true
onFileChanged: historyFileView.reload()
onLoaded: notificationHistoryWinRect.loadHistory()
onLoadFailed: function(error) {
historyAdapter.notifications = [];
historyFileView.writeAdapter();
}
Component.onCompleted: {
if (path) {
reload();
}
}
JsonAdapter {
id: historyAdapter
property var notifications: []
}
}
Rectangle {
width: notificationHistoryWinRect.width
height: notificationHistoryWinRect.height
@ -184,6 +202,7 @@ PanelWithOverlay {
RowLayout {
id: headerRow
spacing: 4
anchors.topMargin: 4
anchors.left: parent.left
@ -193,6 +212,7 @@ PanelWithOverlay {
Layout.preferredHeight: 52
anchors.leftMargin: 16
anchors.rightMargin: 16
Text {
text: "Notification History"
font.pixelSize: 18
@ -200,11 +220,14 @@ PanelWithOverlay {
color: Theme.textPrimary
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
Rectangle {
id: clearAllButton
width: 90
height: 32
radius: 16
@ -212,9 +235,11 @@ PanelWithOverlay {
border.color: Theme.accentPrimary
border.width: 1
Layout.alignment: Qt.AlignVCenter
Row {
anchors.centerIn: parent
spacing: 6
Text {
text: "delete_sweep"
font.family: "Material Symbols Outlined"
@ -222,6 +247,7 @@ PanelWithOverlay {
color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
}
Text {
text: "Clear"
font.pixelSize: Theme.fontSizeSmall
@ -229,15 +255,20 @@ PanelWithOverlay {
font.bold: true
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
id: clearAllMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: notificationHistoryWinRect.clearHistory()
}
}
}
Rectangle {
@ -261,29 +292,36 @@ PanelWithOverlay {
radius: 20
z: 0
}
Rectangle {
id: listContainer
anchors.fill: parent
anchors.topMargin: 12
anchors.bottomMargin: 12
color: "transparent"
clip: true
Column {
anchors.fill: parent
spacing: 0
ListView {
id: historyList
width: parent.width
height: Math.min(contentHeight, parent.height)
spacing: 12
model: historyModel.count > 0 ? historyModel : placeholderModel
clip: true
delegate: Item {
width: parent.width
height: notificationCard.implicitHeight + 12
Rectangle {
id: notificationCard
width: parent.width - 24
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.backgroundPrimary
@ -292,16 +330,22 @@ PanelWithOverlay {
anchors.bottom: parent.bottom
anchors.margins: 0
implicitHeight: contentColumn.implicitHeight + 20
Column {
id: contentColumn
anchors.fill: parent
anchors.margins: 14
spacing: 6
RowLayout {
id: headerRow2
spacing: 8
Rectangle {
id: iconBackground
width: 28
height: 28
radius: 20
@ -309,6 +353,7 @@ PanelWithOverlay {
border.color: Qt.darker(Theme.accentPrimary, 1.2)
border.width: 1.2
Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
text: model.appName ? model.appName.charAt(0).toUpperCase() : "?"
@ -317,11 +362,15 @@ PanelWithOverlay {
font.bold: true
color: Theme.backgroundPrimary
}
}
Column {
id: appInfoColumn
spacing: 0
Layout.alignment: Qt.AlignVCenter
Text {
text: model.appName || "No Notifications"
font.bold: true
@ -330,6 +379,7 @@ PanelWithOverlay {
font.pixelSize: Theme.fontSizeSmall
verticalAlignment: Text.AlignVCenter
}
Text {
visible: !model.isPlaceholder
text: model.timestamp ? notificationHistoryWinRect.formatTimestamp(model.timestamp) : ""
@ -338,11 +388,15 @@ PanelWithOverlay {
font.pixelSize: Theme.fontSizeCaption
verticalAlignment: Text.AlignVCenter
}
}
Item {
Layout.fillWidth: true
}
}
Text {
text: model.summary || (model.isPlaceholder ? "You're all caught up!" : "")
color: Theme.textSecondary
@ -351,6 +405,7 @@ PanelWithOverlay {
width: parent.width
wrapMode: Text.Wrap
}
Text {
text: model.body || (model.isPlaceholder ? "No notifications to show." : "")
color: Theme.textDisabled
@ -359,12 +414,19 @@ PanelWithOverlay {
width: parent.width
wrapMode: Text.Wrap
}
}
}
}
}
}
}
}
Rectangle {
@ -375,14 +437,20 @@ PanelWithOverlay {
ListModel {
id: placeholderModel
ListElement {
appName: ""
summary: ""
body: ""
isPlaceholder: true
}
}
}
}
}
}

View file

@ -155,6 +155,11 @@ PanelWithOverlay {
// Center the settings window on screen
anchors.centerIn: parent
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
Rectangle {
id: background

View file

@ -3,23 +3,14 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Components
import qs.Settings
import qs.Widgets.SettingsWindow
import qs.Components
PanelWithOverlay {
id: sidebarPopup
property var shell: null
// Trigger initial weather loading when component is completed
Component.onCompleted: {
// Load initial weather data after a short delay to ensure all components are ready
Qt.callLater(function() {
if (weather && weather.fetchCityWeather) {
weather.fetchCityWeather();
}
});
}
property var shell: null
function showAt() {
sidebarPopupRect.showAt();
@ -37,18 +28,27 @@ PanelWithOverlay {
sidebarPopupRect.hidePopup();
}
Rectangle {
id: sidebarPopupRect
implicitWidth: 500
implicitHeight: 800
visible: parent.visible
color: "transparent"
anchors.top: parent.top
anchors.right: parent.right
// Trigger initial weather loading when component is completed
Component.onCompleted: {
// Load initial weather data after a short delay to ensure all components are ready
Qt.callLater(function() {
if (weather && weather.fetchCityWeather)
weather.fetchCityWeather();
});
}
Rectangle {
// Access the shell's SettingsWindow instead of creating a new one
id: sidebarPopupRect
property real slideOffset: width
property bool isAnimating: false
property int leftPadding: 20
property int bottomPadding: 20
// Recording properties
property bool isRecording: false
function showAt() {
if (!sidebarPopup.visible) {
@ -59,24 +59,26 @@ PanelWithOverlay {
slideAnim.running = true;
if (weather)
weather.startWeatherFetch();
if (systemWidget)
systemWidget.panelVisible = true;
if (quickAccessWidget)
quickAccessWidget.panelVisible = true;
}
}
function hidePopup() {
if (shell && shell.settingsWindow && shell.settingsWindow.visible) {
if (shell && shell.settingsWindow && shell.settingsWindow.visible)
shell.settingsWindow.visible = false;
}
if (wifiPanelLoader.active && wifiPanelLoader.item && wifiPanelLoader.item.visible) {
if (wifiPanelLoader.active && wifiPanelLoader.item && wifiPanelLoader.item.visible)
wifiPanelLoader.item.visible = false;
}
if (bluetoothPanelLoader.active && bluetoothPanelLoader.item && bluetoothPanelLoader.item.visible) {
if (bluetoothPanelLoader.active && bluetoothPanelLoader.item && bluetoothPanelLoader.item.visible)
bluetoothPanelLoader.item.visible = false;
}
if (sidebarPopup.visible) {
slideAnim.from = 0;
slideAnim.to = width;
@ -84,37 +86,87 @@ PanelWithOverlay {
}
}
// Start screen recording using Quickshell.execDetached
function startRecording() {
var currentDate = new Date();
var hours = String(currentDate.getHours()).padStart(2, '0');
var minutes = String(currentDate.getMinutes()).padStart(2, '0');
var day = String(currentDate.getDate()).padStart(2, '0');
var month = String(currentDate.getMonth() + 1).padStart(2, '0');
var year = currentDate.getFullYear();
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4";
var videoPath = Settings.settings.videoPath;
if (videoPath && !videoPath.endsWith("/"))
videoPath += "/";
var outputPath = videoPath + filename;
var command = "gpu-screen-recorder -w portal" + " -f " + Settings.settings.recordingFrameRate + " -a default_output" + " -k " + Settings.settings.recordingCodec + " -ac " + Settings.settings.audioCodec + " -q " + Settings.settings.recordingQuality + " -cursor " + (Settings.settings.showCursor ? "yes" : "no") + " -cr " + Settings.settings.colorRange + " -o " + outputPath;
Quickshell.execDetached(["sh", "-c", command]);
isRecording = true;
quickAccessWidget.isRecording = true;
}
// Stop recording using Quickshell.execDetached
function stopRecording() {
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]);
// Optionally, force kill after a delay
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect);
cleanupTimer.triggered.connect(function() {
Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]);
cleanupTimer.destroy();
});
isRecording = false;
quickAccessWidget.isRecording = false;
}
implicitWidth: 500
implicitHeight: 800
visible: parent.visible
color: "transparent"
anchors.top: parent.top
anchors.right: parent.right
// Clean up processes on destruction
Component.onDestruction: {
if (isRecording)
stopRecording();
}
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
NumberAnimation {
id: slideAnim
target: sidebarPopupRect
property: "slideOffset"
duration: 300
easing.type: Easing.OutCubic
onStopped: {
if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) {
sidebarPopup.visible = false;
if (weather)
weather.stopWeatherFetch();
if (systemWidget)
systemWidget.panelVisible = false;
if (quickAccessWidget)
quickAccessWidget.panelVisible = false;
}
sidebarPopupRect.isAnimating = false;
}
onStarted: {
sidebarPopupRect.isAnimating = true;
}
}
property int leftPadding: 20
property int bottomPadding: 20
Rectangle {
id: mainRectangle
width: sidebarPopupRect.width - sidebarPopupRect.leftPadding
height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding
anchors.top: sidebarPopupRect.top
@ -126,68 +178,69 @@ PanelWithOverlay {
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
// Access the shell's SettingsWindow instead of creating a new one
}
}
// LazyLoader for WifiPanel
LazyLoader {
id: wifiPanelLoader
loading: false
component: WifiPanel {}
component: WifiPanel {
}
}
// LazyLoader for BluetoothPanel
LazyLoader {
id: bluetoothPanelLoader
loading: false
component: BluetoothPanel {}
component: BluetoothPanel {
}
}
// SettingsIcon component
SettingsIcon {
id: settingsModal
onWeatherRefreshRequested: {
if (weather && weather.fetchCityWeather) {
if (weather && weather.fetchCityWeather)
weather.fetchCityWeather();
}
}
}
Item {
anchors.fill: mainRectangle
x: sidebarPopupRect.slideOffset
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Keys.onEscapePressed: sidebarPopupRect.hidePopup()
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
System {
PowerMenu {
id: systemWidget
Layout.alignment: Qt.AlignHCenter
z: 3
}
Weather {
id: weather
Layout.alignment: Qt.AlignHCenter
z: 2
}
@ -204,8 +257,10 @@ PanelWithOverlay {
SystemMonitor {
id: systemMonitor
z: 2
}
}
// Power profile, Wifi and Bluetooth row
@ -236,6 +291,7 @@ PanelWithOverlay {
// Wifi button
Rectangle {
id: wifiButton
width: 36
height: 36
radius: 18
@ -255,16 +311,17 @@ PanelWithOverlay {
MouseArea {
id: wifiButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!wifiPanelLoader.active) {
if (!wifiPanelLoader.active)
wifiPanelLoader.loading = true;
}
if (wifiPanelLoader.item) {
if (wifiPanelLoader.item)
wifiPanelLoader.item.showAt();
}
}
}
@ -273,11 +330,13 @@ PanelWithOverlay {
targetItem: wifiButtonArea
tooltipVisible: wifiButtonArea.containsMouse
}
}
// Bluetooth button
Rectangle {
id: bluetoothButton
width: 36
height: 36
radius: 18
@ -297,16 +356,17 @@ PanelWithOverlay {
MouseArea {
id: bluetoothButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!bluetoothPanelLoader.active) {
if (!bluetoothPanelLoader.active)
bluetoothPanelLoader.loading = true;
}
if (bluetoothPanelLoader.item) {
if (bluetoothPanelLoader.item)
bluetoothPanelLoader.item.showAt();
}
}
}
@ -315,9 +375,13 @@ PanelWithOverlay {
targetItem: bluetoothButtonArea
tooltipVisible: bluetoothButtonArea.containsMouse
}
}
}
}
}
Item {
@ -326,101 +390,60 @@ PanelWithOverlay {
// QuickAccess widget
QuickAccess {
// 6 is the wallpaper tab index
id: quickAccessWidget
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -16
z: 2
isRecording: sidebarPopupRect.isRecording
onRecordingRequested: {
sidebarPopupRect.startRecording();
}
onStopRecordingRequested: {
sidebarPopupRect.stopRecording();
}
onRecordingStateMismatch: function (actualState) {
onRecordingStateMismatch: function(actualState) {
isRecording = actualState;
quickAccessWidget.isRecording = actualState;
}
onSettingsRequested: {
// Use the SettingsModal's openSettings function
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) {
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings)
settingsModal.openSettings();
}
}
}
onWallpaperSelectorRequested: {
// Use the SettingsModal's openSettings function with wallpaper tab (index 6)
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) {
settingsModal.openSettings(6); // 6 is the wallpaper tab index
}
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings)
settingsModal.openSettings(6);
}
}
}
Keys.onEscapePressed: sidebarPopupRect.hidePopup()
}
// Recording properties
property bool isRecording: false
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
// Start screen recording using Quickshell.execDetached
function startRecording() {
var currentDate = new Date();
var hours = String(currentDate.getHours()).padStart(2, '0');
var minutes = String(currentDate.getMinutes()).padStart(2, '0');
var day = String(currentDate.getDate()).padStart(2, '0');
var month = String(currentDate.getMonth() + 1).padStart(2, '0');
var year = currentDate.getFullYear();
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4";
var videoPath = Settings.settings.videoPath;
if (videoPath && !videoPath.endsWith("/")) {
videoPath += "/";
}
var outputPath = videoPath + filename;
var command = "gpu-screen-recorder -w portal" +
" -f " + Settings.settings.recordingFrameRate +
" -a default_output" +
" -k " + Settings.settings.recordingCodec +
" -ac " + Settings.settings.audioCodec +
" -q " + Settings.settings.recordingQuality +
" -cursor " + (Settings.settings.showCursor ? "yes" : "no") +
" -cr " + Settings.settings.colorRange +
" -o " + outputPath;
Quickshell.execDetached(["sh", "-c", command]);
isRecording = true;
quickAccessWidget.isRecording = true;
}
// Stop recording using Quickshell.execDetached
function stopRecording() {
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]);
// Optionally, force kill after a delay
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect);
cleanupTimer.triggered.connect(function () {
Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]);
cleanupTimer.destroy();
});
isRecording = false;
quickAccessWidget.isRecording = false;
}
// Clean up processes on destruction
Component.onDestruction: {
if (isRecording) {
stopRecording();
}
}
Loader {
active: Settings.settings.showCorners
anchors.fill: parent
sourceComponent: Item {
Corners {
id: sidebarCornerLeft
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
@ -430,15 +453,19 @@ PanelWithOverlay {
Behavior on offsetX {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
Corners {
id: sidebarCornerBottom
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
@ -448,13 +475,20 @@ PanelWithOverlay {
Behavior on offsetX {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
}
}
}
}

View file

@ -1,26 +1,64 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Components
import qs.Helpers
import qs.Services
import qs.Settings
import qs.Widgets
import qs.Widgets.LockScreen
import qs.Helpers
import qs.Services
import qs.Components
Rectangle {
id: systemWidget
property string uptimeText: "--:--"
property bool panelVisible: false
function logout() {
if (WorkspaceManager.isNiri)
logoutProcessNiri.running = true;
else if (WorkspaceManager.isHyprland)
logoutProcessHyprland.running = true;
else
console.warn("No supported compositor detected for logout");
}
function suspend() {
suspendProcess.running = true;
}
function shutdown() {
shutdownProcess.running = true;
}
function reboot() {
rebootProcess.running = true;
}
function updateSystemInfo() {
uptimeProcess.running = true;
}
width: 440
height: 80
color: "transparent"
anchors.horizontalCenterOffset: -2
onPanelVisibleChanged: {
if (panelVisible)
updateSystemInfo();
}
Component.onCompleted: {
uptimeProcess.running = true;
}
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
@ -30,19 +68,16 @@ Rectangle {
anchors.margins: 18
spacing: 12
RowLayout {
Layout.fillWidth: true
spacing: 12
Rectangle {
width: 48
height: 48
radius: 24
color: Theme.accentPrimary
Rectangle {
anchors.fill: parent
color: "transparent"
@ -52,9 +87,10 @@ Rectangle {
z: 2
}
Avatar {}
}
Avatar {
}
}
ColumnLayout {
spacing: 4
@ -74,16 +110,16 @@ Rectangle {
font.pixelSize: 12
color: Theme.textSecondary
}
}
}
Item {
Layout.fillWidth: true
}
Rectangle {
id: systemButton
width: 32
height: 32
radius: 16
@ -101,6 +137,7 @@ Rectangle {
MouseArea {
id: systemButtonArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
@ -108,24 +145,30 @@ Rectangle {
systemMenu.visible = !systemMenu.visible;
}
}
StyledTooltip {
id: systemTooltip
text: "System"
text: "Power Menu"
targetItem: systemButton
tooltipVisible: systemButtonArea.containsMouse
}
}
}
}
}
PanelWithOverlay {
id: systemMenu
anchors.top: systemButton.bottom
anchors.right: systemButton.right
Rectangle {
width: 160
height: 220
color: Theme.surface
@ -136,17 +179,19 @@ Rectangle {
z: 9999
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: 32
anchors.topMargin: systemButton.y + systemButton.height + 48
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
@ -172,10 +217,12 @@ Rectangle {
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: lockButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@ -184,8 +231,8 @@ Rectangle {
systemMenu.visible = false;
}
}
}
}
Rectangle {
Layout.fillWidth: true
@ -211,10 +258,12 @@ Rectangle {
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: suspendButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@ -223,8 +272,8 @@ Rectangle {
systemMenu.visible = false;
}
}
}
}
Rectangle {
Layout.fillWidth: true
@ -251,10 +300,12 @@ Rectangle {
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: rebootButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@ -263,8 +314,8 @@ Rectangle {
systemMenu.visible = false;
}
}
}
}
Rectangle {
Layout.fillWidth: true
@ -290,10 +341,12 @@ Rectangle {
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: logoutButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@ -302,8 +355,8 @@ Rectangle {
systemMenu.visible = false;
}
}
}
}
Rectangle {
Layout.fillWidth: true
@ -329,10 +382,12 @@ Rectangle {
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: shutdownButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@ -341,96 +396,72 @@ Rectangle {
systemMenu.visible = false;
}
}
}
}
}
}
property string uptimeText: "--:--"
Process {
id: uptimeProcess
command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"]
running: false
stdout: StdioCollector {
onStreamFinished: {
uptimeText = this.text.trim();
uptimeProcess.running = false;
}
}
}
Process {
id: shutdownProcess
command: ["shutdown", "-h", "now"]
running: false
}
Process {
id: rebootProcess
command: ["reboot"]
running: false
}
Process {
id: suspendProcess
command: ["systemctl", "suspend"]
running: false
}
Process {
id: logoutProcessNiri
command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
running: false
}
Process {
id: logoutProcessHyprland
command: ["hyprctl", "dispatch", "exit"]
running: false
}
Process {
id: logoutProcess
command: ["loginctl", "terminate-user", Quickshell.env("USER")]
running: false
}
function logout() {
if (WorkspaceManager.isNiri) {
logoutProcessNiri.running = true;
} else if (WorkspaceManager.isHyprland) {
logoutProcessHyprland.running = true;
} else {
console.warn("No supported compositor detected for logout");
}
}
function suspend() {
suspendProcess.running = true;
}
function shutdown() {
shutdownProcess.running = true;
}
function reboot() {
rebootProcess.running = true;
}
property bool panelVisible: false
onPanelVisibleChanged: {
if (panelVisible) {
updateSystemInfo();
}
}
Timer {
interval: 60000
repeat: true
@ -438,16 +469,8 @@ Rectangle {
onTriggered: updateSystemInfo()
}
Component.onCompleted: {
uptimeProcess.running = true;
}
function updateSystemInfo() {
uptimeProcess.running = true;
}
LockScreen {
id: lockScreen
}
}