Merge remote-tracking branch 'upstream/main' into notifications-refine

Resolve conflicts due to project structure changes
This commit is contained in:
FUFSoB
2025-09-24 07:40:50 +05:00
92 changed files with 1872 additions and 1670 deletions
+5 -4
View File
@@ -1,5 +1,5 @@
{
"settingsVersion": 4,
"settingsVersion": 5,
"bar": {
"position": "top",
"backgroundOpacity": 1,
@@ -28,7 +28,7 @@
],
"right": [
{
"id": "ScreenRecorderIndicator"
"id": "ScreenRecorder"
},
{
"id": "Tray"
@@ -58,7 +58,7 @@
"id": "Clock"
},
{
"id": "SidePanelToggle"
"id": "ControlCenter"
}
]
}
@@ -132,7 +132,8 @@
"respectExpireTimeout": false,
"lowUrgencyDuration": 3,
"normalUrgencyDuration": 8,
"criticalUrgencyDuration": 15
"criticalUrgencyDuration": 15,
"enableOSD": true
},
"audio": {
"volumeStep": 5,
-1
View File
@@ -4,7 +4,6 @@ import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Commons.IconsSets
Singleton {
id: root
+22 -10
View File
@@ -53,7 +53,8 @@ Singleton {
// This should only be activated once when the settings structure has changed
// Then it should be commented out again, regular users don't need to generate
// default settings on every start
//generateDefaultSettings()
// TODO: automate this someday!
// generateDefaultSettings()
// Patch-in the local default, resolved to user's home
adapter.general.avatarImage = defaultAvatar
@@ -113,7 +114,7 @@ Singleton {
JsonAdapter {
id: adapter
property int settingsVersion: 4
property int settingsVersion: 5
// bar
property JsonObject bar: JsonObject {
@@ -142,7 +143,7 @@ Singleton {
"id": "Workspace"
}]
property list<var> right: [{
"id": "ScreenRecorderIndicator"
"id": "ScreenRecorder"
}, {
"id": "Tray"
}, {
@@ -162,7 +163,7 @@ Singleton {
}, {
"id": "Clock"
}, {
"id": "SidePanelToggle"
"id": "ControlCenter"
}]
}
}
@@ -254,6 +255,7 @@ Singleton {
property int lowUrgencyDuration: 3
property int normalUrgencyDuration: 8
property int criticalUrgencyDuration: 15
property bool enableOSD: true
}
// audio
@@ -381,15 +383,25 @@ Singleton {
const sections = ["left", "center", "right"]
// -----------------
// 1st. check our settings are not super old, when we only had the widget type as a plain string
// 1st. convert old widget id to new id
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
if (typeof widget === "string") {
adapter.bar.widgets[sectionName][i] = {
"id": widget
}
switch (widget.id) {
case "DarkModeToggle":
widget.id = "DarkMode"
break
case "PowerToggle":
widget.id = "SessionMenu"
break
case "ScreenRecorderIndicator":
widget.id = "ScreenRecorder"
break
case "SidePanelToggle":
widget.id = "ControlCenter"
break
}
}
}
@@ -403,8 +415,8 @@ Singleton {
for (var i = widgets.length - 1; i >= 0; i--) {
var widget = widgets[i]
if (!BarWidgetRegistry.hasWidget(widget.id)) {
widgets.splice(i, 1)
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
widgets.splice(i, 1)
}
}
}
@@ -56,6 +56,9 @@ Singleton {
"keep-awake-on": "mug",
"keep-awake-off": "mug-off",
"disc": "disc-filled",
"eye": "eye",
"pin": "pin",
"unpin": "pinned-off",
"image": "photo",
"dark-mode": "contrast-filled",
"camera-video": "video",
@@ -119,28 +122,6 @@ Singleton {
"bt-device-watch": "device-watch",
"bt-device-speaker": "device-speaker",
"bt-device-tv": "device-tv",
"filepicker-folder": "folder",
"filepicker-refresh": "refresh",
"filepicker-close": "x",
"filepicker-arrow-left": "arrow-left",
"filepicker-arrow-up": "arrow-up",
"filepicker-home": "home",
"filepicker-layout-grid": "layout-grid",
"filepicker-list": "list",
"filepicker-search": "search",
"filepicker-x": "x",
"filepicker-photo": "photo",
"filepicker-check": "check",
"filepicker-file-text": "file-text",
"filepicker-video": "video",
"filepicker-music": "music",
"filepicker-archive": "archive",
"filepicker-table": "table",
"filepicker-presentation": "presentation",
"filepicker-code": "code",
"filepicker-settings": "settings",
"filepicker-file": "file",
"filepicker-text": "file-text",
"noctalia": "noctalia"
}
-1
View File
@@ -3,7 +3,6 @@ import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Modules.SettingsPanel
import qs.Widgets
Variants {
+6 -5
View File
@@ -31,6 +31,7 @@ Item {
}
readonly property string windowTitle: CompositorService.getFocusedWindowTitle()
readonly property bool hasActiveWindow: windowTitle !== ""
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
@@ -52,7 +53,7 @@ Item {
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
visible: windowTitle !== ""
visible: hasActiveWindow
function calculatedVerticalHeight() {
// Use standard widget height like other widgets
@@ -86,7 +87,7 @@ Item {
try {
const idValue = focusedWindow.appId
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase())
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase())
if (iconResult && iconResult !== "") {
return iconResult
}
@@ -102,7 +103,7 @@ Item {
if (activeToplevel.appId) {
const idValue2 = activeToplevel.appId
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
const iconResult2 = AppIcons.iconForAppId(normalizedId2.toLowerCase())
const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase())
if (iconResult2 && iconResult2 !== "") {
return iconResult2
}
@@ -159,7 +160,7 @@ Item {
Layout.preferredWidth: Style.capsuleHeight * 0.75 * scaling
Layout.preferredHeight: Style.capsuleHeight * 0.75 * scaling
Layout.alignment: Qt.AlignVCenter
visible: windowTitle !== "" && showIcon
visible: hasActiveWindow && showIcon
IconImage {
id: windowIcon
@@ -217,7 +218,7 @@ Item {
anchors.centerIn: parent
width: parent.width - Style.marginXS * scaling * 2
height: parent.height - Style.marginXS * scaling * 2
visible: barPosition === "left" || barPosition === "right"
visible: (barPosition === "left" || barPosition === "right") && hasActiveWindow
// Window icon
Item {
+2 -2
View File
@@ -1,10 +1,10 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Bar.Extras
import qs.Modules.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
@@ -43,7 +43,7 @@ NIconButton {
colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary
colorBorder: Color.transparent
colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent
onClicked: PanelService.getPanel("sidePanel")?.toggle(this)
onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
IconImage {
@@ -52,9 +52,11 @@ NIconButton {
width: root.width * 0.8
height: width
source: {
if (customIconPath !== "") return customIconPath;
if (useDistroLogo) return DistroLogoService.osLogo;
return "";
if (customIconPath !== "")
return customIconPath
if (useDistroLogo)
return DistroLogoService.osLogo
return ""
}
visible: source !== ""
smooth: true
+1 -1
View File
@@ -5,7 +5,7 @@ import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Modules.Bar.Extras
Item {
+3 -3
View File
@@ -100,7 +100,7 @@ Item {
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying
active: showVisualizer && visualizerType == "linear"
z: 0
sourceComponent: LinearSpectrum {
@@ -115,7 +115,7 @@ Item {
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying
active: showVisualizer && visualizerType == "mirrored"
z: 0
sourceComponent: MirroredSpectrum {
@@ -130,7 +130,7 @@ Item {
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying
active: showVisualizer && visualizerType == "wave"
z: 0
sourceComponent: WaveSpectrum {
+1 -1
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
+1 -1
View File
@@ -4,7 +4,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Services
import qs.Widgets
@@ -14,10 +14,10 @@ NIconButton {
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
icon: "power"
tooltipText: "Power panel"
tooltipText: "Session menu"
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mError
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("powerPanel")?.toggle()
onClicked: PanelService.getPanel("sessionMenuPanel")?.toggle()
}
+1 -1
View File
@@ -68,7 +68,7 @@ Rectangle {
anchors.centerIn: parent
width: parent.width
height: parent.height
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
asynchronous: true
}
+1 -1
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
+1 -1
View File
@@ -19,5 +19,5 @@ NIconButton {
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("wallpaperSelector")?.toggle(this)
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
}
+66
View File
@@ -56,6 +56,10 @@ Item {
property int horizontalPadding: Math.round(Style.marginS * scaling)
property int spacingBetweenPills: Math.round(Style.marginXS * scaling)
// Wheel scroll handling
property int wheelAccumulatedDelta: 0
property bool wheelCooldown: false
signal workspaceChanged(int workspaceId, color accentColor)
implicitWidth: isVertical ? Math.round(Style.barHeight * scaling) : computeWidth()
@@ -95,6 +99,28 @@ Item {
return Math.round(total)
}
function getFocusedLocalIndex() {
for (var i = 0; i < localWorkspaces.count; i++) {
if (localWorkspaces.get(i).isFocused === true)
return i
}
return -1
}
function switchByOffset(offset) {
if (localWorkspaces.count === 0)
return
var current = getFocusedLocalIndex()
if (current < 0)
current = 0
var next = (current + offset) % localWorkspaces.count
if (next < 0)
next = localWorkspaces.count - 1
const ws = localWorkspaces.get(next)
if (ws && ws.idx !== undefined)
CompositorService.switchToWorkspace(ws.idx)
}
Component.onCompleted: {
refreshWorkspaces()
}
@@ -185,6 +211,46 @@ Item {
anchors.verticalCenter: parent.verticalCenter
}
// Debounce timer for wheel interactions
Timer {
id: wheelDebounce
interval: 150
repeat: false
onTriggered: {
root.wheelCooldown = false
root.wheelAccumulatedDelta = 0
}
}
// Scroll to switch workspaces
WheelHandler {
id: wheelHandler
target: root
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: function (event) {
if (root.wheelCooldown)
return
// Prefer vertical delta, fall back to horizontal if needed
var dy = event.angleDelta.y
var dx = event.angleDelta.x
var useDy = Math.abs(dy) >= Math.abs(dx)
var delta = useDy ? dy : dx
// One notch is typically 120
root.wheelAccumulatedDelta += delta
var step = 120
if (Math.abs(root.wheelAccumulatedDelta) >= step) {
var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1
// For vertical layout, natural mapping: wheel up -> previous, down -> next (already handled by sign)
// For horizontal layout, same mapping using vertical wheel
root.switchByOffset(direction)
root.wheelCooldown = true
wheelDebounce.restart()
root.wheelAccumulatedDelta = 0
event.accepted = true
}
}
}
// Horizontal layout for top/bottom bars
Row {
id: pillRow
@@ -32,12 +32,12 @@ NBox {
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No media player detected"
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
// NText {
// text: "No media player detected"
// color: Color.mOnSurfaceVariant
// Layout.alignment: Qt.AlignHCenter
// }
Item {
Layout.fillWidth: true
Layout.fillHeight: true
@@ -51,91 +51,71 @@ NBox {
visible: MediaService.currentPlayer && MediaService.canPlay
spacing: Style.marginM * scaling
// Player selector
ComboBox {
id: playerSelector
// Player selector using NContextMenu
Rectangle {
id: playerSelectorButton
Layout.fillWidth: true
Layout.preferredHeight: Style.barHeight * 0.83 * scaling
Layout.preferredHeight: Style.barHeight * scaling
visible: MediaService.getAvailablePlayers().length > 1
model: MediaService.getAvailablePlayers()
textRole: "identity"
currentIndex: MediaService.selectedPlayerIndex
radius: Style.radiusM * scaling
color: Color.transparent
background: Rectangle {
visible: false
// implicitWidth: 120 * scaling
// implicitHeight: 30 * scaling
color: Color.transparent
border.color: playerSelector.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
}
property var currentPlayer: MediaService.getAvailablePlayers()[MediaService.selectedPlayerIndex]
contentItem: NText {
visible: false
leftPadding: Style.marginM * scaling
rightPadding: playerSelector.indicator.width + playerSelector.spacing
text: playerSelector.displayText
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
RowLayout {
anchors.fill: parent
spacing: Style.marginS * scaling
indicator: NIcon {
x: playerSelector.width - width
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
icon: "caret-down"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurface
horizontalAlignment: Text.AlignRight
}
popup: Popup {
id: popup
x: playerSelector.width * 0.5
y: playerSelector.height * 0.75
width: playerSelector.width * 0.5
implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginM * scaling)
padding: Style.marginS * scaling
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: playerSelector.popup.visible ? playerSelector.delegateModel : null
currentIndex: playerSelector.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {}
NIcon {
icon: "caret-down"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurfaceVariant
}
background: Rectangle {
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusXS * scaling
NText {
text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : ""
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
delegate: ItemDelegate {
width: playerSelector.width
contentItem: NText {
text: modelData.identity
font.pointSize: Style.fontSizeS * scaling
color: highlighted ? Color.mSurface : Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: playerSelector.highlightedIndex === index
MouseArea {
id: playerSelectorMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
background: Rectangle {
width: popup.width - Style.marginS * scaling * 2
color: highlighted ? Color.mSecondary : Color.transparent
radius: Style.radiusXS * scaling
onClicked: {
// Create menu items from available players
var menuItems = []
var players = MediaService.getAvailablePlayers()
for (var i = 0; i < players.length; i++) {
menuItems.push({
"label": players[i].identity,
"action": i.toString(),
"icon": "disc",
"enabled": true,
"visible": true
})
}
playerContextMenu.model = menuItems
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height)
}
}
onActivated: {
MediaService.selectedPlayerIndex = currentIndex
MediaService.updateCurrentPlayer()
NContextMenu {
id: playerContextMenu
parent: root
width: 200 * scaling
onTriggered: function (action) {
var index = parseInt(action)
if (!isNaN(index)) {
MediaService.selectedPlayerIndex = index
MediaService.updateCurrentPlayer()
}
}
}
}
@@ -193,7 +173,7 @@ NBox {
NText {
visible: MediaService.trackArtist !== ""
text: MediaService.trackArtist
color: Color.mOnSurface
color: Color.mPrimary
font.pointSize: Style.fontSizeXS * scaling
elide: Text.ElideRight
Layout.fillWidth: true
@@ -324,7 +304,7 @@ NBox {
}
Loader {
active: Settings.data.audio.visualizerType == "linear" && MediaService.isPlaying
active: Settings.data.audio.visualizerType == "linear"
Layout.alignment: Qt.AlignHCenter
sourceComponent: LinearSpectrum {
@@ -337,7 +317,7 @@ NBox {
}
Loader {
active: Settings.data.audio.visualizerType == "mirrored" && MediaService.isPlaying
active: Settings.data.audio.visualizerType == "mirrored"
Layout.alignment: Qt.AlignHCenter
sourceComponent: MirroredSpectrum {
@@ -350,7 +330,7 @@ NBox {
}
Loader {
active: Settings.data.audio.visualizerType == "wave" && MediaService.isPlaying
active: Settings.data.audio.visualizerType == "wave"
Layout.alignment: Qt.AlignHCenter
sourceComponent: WaveSpectrum {
@@ -4,8 +4,8 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Modules.SettingsPanel
import qs.Modules.SidePanel
import qs.Modules.Settings
import qs.Modules.ControlCenter
import qs.Commons
import qs.Services
import qs.Widgets
@@ -66,10 +66,10 @@ NBox {
NIconButton {
id: powerButton
icon: "power"
tooltipText: "Power panel"
tooltipText: "Session Menu"
onClicked: {
powerPanel.open()
sidePanel.close()
sessionMenuPanel.open()
controlCenterPanel.close()
}
}
@@ -78,7 +78,7 @@ NBox {
icon: "close"
tooltipText: "Close side panel"
onClicked: {
sidePanel.close()
controlCenterPanel.close()
}
}
}
@@ -3,7 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Services
import qs.Widgets
@@ -33,8 +33,8 @@ NBox {
ScreenRecorderService.toggleRecording()
// If we were not recording and we just initiated a start, close the panel
if (!ScreenRecorderService.isRecording) {
var panel = PanelService.getPanel("sidePanel")
panel && panel.close()
var panel = PanelService.getPanel("controlCenterPanel")
panel?.close()
}
}
}
@@ -55,7 +55,7 @@ NBox {
visible: Settings.data.wallpaper.enabled
icon: "wallpaper-selector"
tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper."
onClicked: PanelService.getPanel("wallpaperSelector")?.toggle(this)
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
onRightClicked: WallpaperService.setRandomWallpaper()
}
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Modules.SidePanel.Cards
import qs.Modules.ControlCenter.Cards
import qs.Commons
import qs.Services
import qs.Widgets
+48 -42
View File
@@ -34,14 +34,6 @@ Variants {
}
}
// Also listen to model changes (for ObjectModel)
Connections {
target: ToplevelManager ? ToplevelManager.toplevels : null
function onCountChanged() {
updateDockApps()
}
}
// Update dock apps when pinned apps change
Connections {
target: Settings.data.dock
@@ -108,19 +100,28 @@ Variants {
const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : []
const pinnedApps = Settings.data.dock.pinnedApps || []
const combined = []
const processedAppIds = new Set()
// First, add pinned apps (both running and non-running) in their pinned order
// Strategy: Maintain app positions as much as possible
// 1. First pass: Add all running apps (both pinned and non-pinned) in their current order
runningApps.forEach(toplevel => {
if (toplevel && toplevel.appId) {
const isPinned = pinnedApps.includes(toplevel.appId)
const appType = isPinned ? "pinned-running" : "running"
combined.push({
"type": appType,
"toplevel": toplevel,
"appId": toplevel.appId,
"title": toplevel.title
})
processedAppIds.add(toplevel.appId)
}
})
// 2. Second pass: Add non-running pinned apps at the end
pinnedApps.forEach(pinnedAppId => {
const runningApp = runningApps.find(toplevel => toplevel && toplevel.appId === pinnedAppId)
if (runningApp) {
// Pinned app that is currently running
combined.push({
"type": "pinned-running",
"toplevel": runningApp,
"appId": runningApp.appId,
"title": runningApp.title
})
} else {
if (!processedAppIds.has(pinnedAppId)) {
// Pinned app that is not running
combined.push({
"type": "pinned",
@@ -131,21 +132,6 @@ Variants {
}
})
// Then, add running apps that are not pinned
runningApps.forEach(toplevel => {
if (toplevel && toplevel.appId) {
const isPinned = pinnedApps.includes(toplevel.appId)
if (!isPinned) {
combined.push({
"type": "running",
"toplevel": toplevel,
"appId": toplevel.appId,
"title": toplevel.title
})
}
}
})
dockApps = combined
}
@@ -355,7 +341,7 @@ Variants {
function getAppIcon(appData): string {
if (!appData || !appData.appId)
return ""
return AppIcons.iconForAppId(appData.appId?.toLowerCase())
return ThemeIcons.iconForAppId(appData.appId?.toLowerCase())
}
RowLayout {
@@ -462,13 +448,25 @@ Variants {
id: contextMenu
scaling: root.scaling
onHoveredChanged: menuHovered = hovered
onRequestClose: contextMenu.hide()
onRequestClose: {
contextMenu.hide()
// Restart hide timer after menu action if auto-hide is enabled
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
hideTimer.restart()
}
}
onAppClosed: root.updateDockApps // Force immediate dock update when app is closed
onVisibleChanged: {
if (visible) {
root.currentContextMenu = contextMenu
} else if (root.currentContextMenu === contextMenu) {
root.currentContextMenu = null
// Reset menu hover state when menu becomes invisible
menuHovered = false
// Restart hide timer if conditions are met
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
hideTimer.restart()
}
}
}
}
@@ -501,11 +499,23 @@ Variants {
}
onClicked: function (mouse) {
// Close any existing context menu first
if (mouse.button !== Qt.RightButton || root.currentContextMenu !== contextMenu) {
if (mouse.button === Qt.RightButton) {
// If right-clicking on the same app with an open context menu, close it
if (root.currentContextMenu === contextMenu && contextMenu.visible) {
root.closeAllContextMenus()
return
}
// Close any other existing context menu first
root.closeAllContextMenus()
// Hide tooltip when showing context menu
appTooltip.hide()
contextMenu.show(appButton, modelData.toplevel || modelData)
return
}
// Close any existing context menu for non-right-click actions
root.closeAllContextMenus()
// Check if toplevel is still valid (not a stale reference)
const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel)
@@ -520,10 +530,6 @@ Variants {
// Pinned app not running - launch it
Quickshell.execDetached(["gtk-launch", modelData.appId])
}
} else if (mouse.button === Qt.RightButton) {
// Hide tooltip when showing context menu
appTooltip.hide()
contextMenu.show(appButton, modelData.toplevel || modelData)
}
}
}
+27 -24
View File
@@ -19,7 +19,7 @@ PopupWindow {
signal requestClose
implicitWidth: 160 * scaling
implicitWidth: 140 * scaling
implicitHeight: contextMenuColumn.implicitHeight + (Style.marginM * scaling * 2)
color: Color.transparent
visible: false
@@ -75,7 +75,14 @@ PopupWindow {
id: menuMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.hide() // Close when clicking on the background (outside menu content)
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) {
root.hide() // Close on right-click
} else {
root.hide() // Close when clicking on the background (outside menu content)
}
}
}
Shortcut {
@@ -86,7 +93,7 @@ PopupWindow {
Rectangle {
anchors.fill: parent
color: Color.mSurface
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
@@ -105,11 +112,11 @@ PopupWindow {
anchors.margins: Style.marginM * scaling
spacing: 0
// Activate/Focus item
// Focus item
Rectangle {
width: parent.width
height: 32 * scaling
color: activateMouseArea.containsMouse ? Qt.alpha(Color.mSecondary, 0.2) : Color.transparent
color: activateMouseArea.containsMouse ? Color.mTertiary : Color.transparent
radius: Style.radiusXS * scaling
Row {
@@ -121,20 +128,14 @@ PopupWindow {
NIcon {
icon: "eye"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
color: activateMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
NText {
text: {
if (!root.toplevel)
return "Activate"
// Check if this toplevel is active by comparing with ToplevelManager.activeToplevel
const isActive = ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === root.toplevel
return isActive ? "Focus" : "Activate"
}
text: "Focus"
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurface
color: activateMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
}
@@ -149,7 +150,7 @@ PopupWindow {
if (root.toplevel?.activate) {
root.toplevel.activate()
}
root.hide()
root.requestClose()
}
}
}
@@ -158,7 +159,7 @@ PopupWindow {
Rectangle {
width: parent.width
height: 32 * scaling
color: pinMouseArea.containsMouse ? Qt.alpha(Color.mTertiary, 0.2) : Color.transparent
color: pinMouseArea.containsMouse ? Color.mTertiary : Color.transparent
radius: Style.radiusXS * scaling
Row {
@@ -171,10 +172,10 @@ PopupWindow {
icon: {
if (!root.toplevel)
return "pin"
return root.isAppPinned(root.toplevel.appId) ? "pinned-off" : "pin"
return root.isAppPinned(root.toplevel.appId) ? "unpin" : "pin"
}
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
color: pinMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
@@ -185,7 +186,7 @@ PopupWindow {
return root.isAppPinned(root.toplevel.appId) ? "Unpin" : "Pin"
}
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurface
color: pinMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
}
@@ -200,7 +201,8 @@ PopupWindow {
if (root.toplevel?.appId) {
root.toggleAppPin(root.toplevel.appId)
}
root.hide()
//root.hide()
root.requestClose()
}
}
}
@@ -209,7 +211,7 @@ PopupWindow {
Rectangle {
width: parent.width
height: 32 * scaling
color: closeMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.2) : Color.transparent
color: closeMouseArea.containsMouse ? Color.mTertiary : Color.transparent
radius: Style.radiusXS * scaling
Row {
@@ -219,16 +221,16 @@ PopupWindow {
spacing: Style.marginS * scaling
NIcon {
icon: "x"
icon: "close"
font.pointSize: Style.fontSizeL * scaling
color: closeMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface
color: closeMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
NText {
text: "Close"
font.pointSize: Style.fontSizeS * scaling
color: closeMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface
color: closeMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
}
@@ -253,6 +255,7 @@ PopupWindow {
Logger.warn("DockMenu", "Cannot close app - invalid toplevel reference")
}
root.hide()
root.requestClose()
}
}
}
+1 -1
View File
@@ -397,7 +397,7 @@ NPanel {
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : ""
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== ""
asynchronous: true
}
+200 -68
View File
@@ -199,7 +199,7 @@ Loader {
z: 10
Loader {
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear"
active: Settings.data.audio.visualizerType == "linear"
anchors.centerIn: parent
width: 160 * scaling
height: 160 * scaling
@@ -228,7 +228,7 @@ Loader {
}
Loader {
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "mirrored"
active: Settings.data.audio.visualizerType == "mirrored"
anchors.centerIn: parent
width: 160 * scaling
height: 160 * scaling
@@ -258,7 +258,7 @@ Loader {
}
Loader {
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "wave"
active: Settings.data.audio.visualizerType == "wave"
anchors.centerIn: parent
width: 160 * scaling
height: 160 * scaling
@@ -305,31 +305,6 @@ Loader {
}
}
Rectangle {
anchors.centerIn: parent
width: parent.width + 24 * scaling
height: parent.height + 24 * scaling
radius: width * 0.5
color: Color.transparent
border.color: Qt.alpha(Color.mPrimary, 0.3)
border.width: Math.max(1, Style.borderM * scaling)
z: -1
visible: !MediaService.isPlaying
SequentialAnimation on scale {
loops: Animation.Infinite
NumberAnimation {
to: 1.1
duration: 1500
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 1500
easing.type: Easing.InOutQuad
}
}
}
NImageCircled {
anchors.centerIn: parent
width: 100 * scaling
@@ -364,6 +339,7 @@ Loader {
Rectangle {
id: terminalBackground
anchors.fill: parent
clip: true
radius: Style.radiusM * scaling
color: Qt.alpha(Color.mSurface, 0.9)
border.color: Color.mPrimary
@@ -558,48 +534,60 @@ Loader {
}
}
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus && !lockContext.unlockInProgress
// Container for asterisks and cursor to control positioning
Item {
Layout.fillWidth: true
Layout.preferredHeight: asterisksText.implicitHeight
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus && !lockContext.unlockInProgress
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
wrapMode: Text.NoWrap
maximumLineCount: 1
elide: Text.ElideRight
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
}
}
}
}
Rectangle {
width: 8 * scaling
height: 20 * scaling
color: Color.mPrimary
visible: passwordInput.activeFocus
Layout.leftMargin: -Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
Rectangle {
width: 8 * scaling
height: 20 * scaling
color: Color.mPrimary
visible: passwordInput.activeFocus
anchors.left: asterisksText.right
anchors.leftMargin: Style.marginXS * scaling
anchors.verticalCenter: parent.verticalCenter
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
}
}
}
}
@@ -643,6 +631,7 @@ Loader {
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling
Layout.fillWidth: true
Rectangle {
Layout.preferredWidth: 120 * scaling
Layout.preferredHeight: 40 * scaling
@@ -731,6 +720,149 @@ Loader {
}
}
// ALARMING Easter Egg for long passwords
Item {
id: easterEggContainer
anchors.fill: parent
z: 1000
property bool easterEggTriggered: false
// Monitor password length
Connections {
target: passwordInput
function onTextChanged() {
if (passwordInput.text.length >= 25) {
easterEggContainer.easterEggTriggered = true
}
}
function onActiveFocusChanged() {
if (!passwordInput.activeFocus) {
easterEggContainer.easterEggTriggered = false
}
}
}
// Also reset when authentication starts
Connections {
target: lockContext
function onUnlockInProgressChanged() {
if (lockContext.unlockInProgress) {
easterEggContainer.easterEggTriggered = false
}
}
}
// Scattered warning messages (game-style pop-ups)
Repeater {
model: easterEggContainer.easterEggTriggered && passwordInput.activeFocus && !lockContext.unlockInProgress ? 12 : 0
NText {
property var messages: ["BREACH DETECTED", "SECURITY ALERT", "SYSTEM COMPROMISED", "ANOMALY DETECTED", "FIREWALL BREACH", "DEFENSE FAILING", "16 // 16 // 16", "THE ATLAS SEES ALL", "SIMULATION DETECTED", "WAKE UP", "16 16 16 16 16", "KZZT... 16... KZZT", "ERROR ERROR ERROR", "THEY'RE WATCHING", "16 MINUTES REMAIN"]
property real baseX: Math.random() * (parent.width - 300)
property real baseY: Math.random() * (parent.height - 80)
text: messages[index % messages.length]
color: Color.mError
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
x: baseX
y: baseY
// Better random positioning avoiding center terminal
Component.onCompleted: {
var centerX = parent.width / 2
var centerY = parent.height / 2
var avoidRadius = 350 * scaling
// If too close to center, push to random edge zones
var distanceFromCenter = Math.sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY))
if (distanceFromCenter < avoidRadius) {
// Pick a random edge zone
var zone = Math.floor(Math.random() * 4)
switch (zone) {
case 0:
// Top
x = Math.random() * parent.width
y = Math.random() * 100 * scaling
break
case 1:
// Right
x = parent.width - (50 + Math.random() * 200) * scaling
y = Math.random() * parent.height
break
case 2:
// Bottom
x = Math.random() * parent.width
y = parent.height - (50 + Math.random() * 100) * scaling
break
case 3:
// Left
x = Math.random() * 200 * scaling
y = Math.random() * parent.height
break
}
}
// Add some random drift to make positioning more varied
x += (Math.random() - 0.5) * 100 * scaling
y += (Math.random() - 0.5) * 50 * scaling
// Ensure we stay within bounds
x = Math.max(20 * scaling, Math.min(parent.width - 280 * scaling, x))
y = Math.max(20 * scaling, Math.min(parent.height - 60 * scaling, y))
}
// Simple pop-in animation
SequentialAnimation on scale {
loops: Animation.Infinite
PauseAnimation {
duration: index * 400 + Math.random() * 1000
}
NumberAnimation {
from: 0
to: 1.2
duration: 300
easing.type: Easing.OutBack
}
NumberAnimation {
to: 1.0
duration: 200
}
PauseAnimation {
duration: 2000 + Math.random() * 3000
}
NumberAnimation {
to: 0
duration: 300
}
PauseAnimation {
duration: 800 + Math.random() * 1200
}
}
// Gentle blinking effect
SequentialAnimation on opacity {
loops: Animation.Infinite
PauseAnimation {
duration: index * 200
}
NumberAnimation {
to: 0.6
duration: 400 + Math.random() * 300
}
NumberAnimation {
to: 1.0
duration: 300 + Math.random() * 200
}
}
}
}
}
// Power buttons at bottom right
RowLayout {
anchors.right: parent.right
@@ -759,7 +891,7 @@ Loader {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
anchors.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
@@ -810,7 +942,7 @@ Loader {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
anchors.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
@@ -862,7 +994,7 @@ Loader {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
anchors.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
@@ -12,7 +12,7 @@ import qs.Widgets
NPanel {
id: root
preferredWidth: 360
preferredWidth: 380
preferredHeight: 480
panelKeyboardFocus: true
@@ -37,7 +37,7 @@ NPanel {
}
NText {
text: "Notification history"
text: "Notifications"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
+316
View File
@@ -0,0 +1,316 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
Loader {
id: windowLoader
active: false
// OSD Type enum
enum Type {
Volume,
Brightness
}
property int osdType: OSD.Type.Volume
readonly property real scaling: ScalingService.getScreenScale(Quickshell.screens[0])
// Volume properties
readonly property real currentVolume: AudioService.volume
readonly property bool isMuted: AudioService.muted
property bool firstVolumeReceived: false
property bool firstMuteReceived: false
// Brightness properties
readonly property real currentBrightness: {
if (BrightnessService.monitors.length > 0) {
return BrightnessService.monitors[0].brightness || 0
}
return 0
}
property bool firstBrightnessReceived: false
// Get appropriate icon based on OSD type
function getIcon() {
if (osdType === OSD.Type.Volume) {
if (AudioService.muted) {
return "volume-mute"
}
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
} else {
// Brightness
var brightness = currentBrightness
return brightness <= 0.5 ? "brightness-low" : "brightness-high"
}
}
// Get current value (0-1 range)
function getCurrentValue() {
if (osdType === OSD.Type.Volume) {
return isMuted ? 0 : currentVolume
} else {
return currentBrightness
}
}
// Get display percentage
function getDisplayPercentage() {
if (osdType === OSD.Type.Volume) {
return isMuted ? "0%" : Math.round(currentVolume * 100) + "%"
} else {
return Math.round(currentBrightness * 100) + "%"
}
}
// Get progress bar color
function getProgressColor() {
if (osdType === OSD.Type.Volume) {
return isMuted ? Color.mError : Color.mPrimary
} else {
return Color.mPrimary
}
}
// Get icon color
function getIconColor() {
if (osdType === OSD.Type.Volume) {
return isMuted ? Color.mError : Color.mOnSurface
} else {
return Color.mOnSurface
}
}
sourceComponent: PanelWindow {
id: panel
screen: Quickshell.screens[0] // Use primary screen
anchors {
top: true
}
implicitWidth: 320 * windowLoader.scaling
implicitHeight: osdItem.height
// Set margins based on bar position
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginS) * windowLoader.scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * windowLoader.scaling : 0)
default:
return Style.marginL * windowLoader.scaling
}
}
color: Color.transparent
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
Rectangle {
id: osdItem
width: parent.width
height: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * windowLoader.scaling)
radius: Style.radiusL * windowLoader.scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(2, Style.borderM * windowLoader.scaling)
visible: false
opacity: 0
scale: 0.7
anchors.horizontalCenter: parent.horizontalCenter
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
Timer {
id: hideTimer
interval: 2000
onTriggered: osdItem.hide()
}
RowLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM * windowLoader.scaling
spacing: Style.marginM * windowLoader.scaling
NIcon {
icon: windowLoader.getIcon()
color: windowLoader.getIconColor()
font.pointSize: Style.fontSizeXL * windowLoader.scaling
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXS * windowLoader.scaling
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.round(6 * windowLoader.scaling)
radius: Math.round(3 * windowLoader.scaling)
color: Color.mSurfaceVariant
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * Math.min(1.0, windowLoader.getCurrentValue())
radius: parent.radius
color: windowLoader.getProgressColor()
Behavior on width {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
NText {
text: windowLoader.getDisplayPercentage()
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * windowLoader.scaling
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: Math.round(32 * windowLoader.scaling)
}
}
}
function show() {
hideTimer.stop()
osdItem.visible = true
osdItem.opacity = 1
osdItem.scale = 1.0
hideTimer.start()
}
function hide() {
hideTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.7
Qt.callLater(function () {
osdItem.visible = false
windowLoader.active = false
})
}
function hideImmediately() {
hideTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.7
osdItem.visible = false
windowLoader.active = false
}
}
function showOSD() {
osdItem.show()
}
}
// Volume change monitoring
Connections {
target: AudioService
enabled: osdType === OSD.Type.Volume
function onVolumeChanged() {
if (!firstVolumeReceived) {
firstVolumeReceived = true
} else {
showOSD()
}
}
function onMutedChanged() {
if (!firstMuteReceived) {
firstMuteReceived = true
} else {
showOSD()
}
}
}
// Brightness change monitoring
Connections {
target: BrightnessService
enabled: osdType === OSD.Type.Brightness
function onMonitorsChanged() {
for (var i = 0; i < BrightnessService.monitors.length; i++) {
let monitor = BrightnessService.monitors[i]
monitor.brightnessUpdated.connect(windowLoader.onBrightnessChanged)
}
}
}
Component.onCompleted: {
if (osdType === OSD.Type.Brightness) {
for (var i = 0; i < BrightnessService.monitors.length; i++) {
let monitor = BrightnessService.monitors[i]
monitor.brightnessUpdated.connect(windowLoader.onBrightnessChanged)
}
}
}
function onBrightnessChanged(newBrightness) {
if (!firstBrightnessReceived) {
firstBrightnessReceived = true
} else {
showOSD()
}
}
// Signal to coordinate with other OSDs
signal osdShowing
function showOSD() {
// Check if OSD is enabled in settings
if (!Settings.data.notifications.enableOSD) {
return
}
osdShowing() // Notify other OSDs to hide
windowLoader.active = true
if (windowLoader.item) {
windowLoader.item.showOSD()
}
}
function hideOSD() {
if (windowLoader.item && windowLoader.item.osdItem) {
windowLoader.item.osdItem.hideImmediately()
} else if (windowLoader.active) {
// If window exists but osdItem isn't ready, just deactivate the loader
windowLoader.active = false
}
}
}
@@ -263,7 +263,7 @@ NPanel {
Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling
NText {
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Power panel"
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Session Menu"
font.weight: Style.fontWeightBold
font.pointSize: Style.fontSizeL * scaling
color: timerActive ? Color.mPrimary : Color.mOnSurface
@@ -20,6 +20,7 @@ NBox {
signal removeWidget(string section, int index)
signal reorderWidget(string section, int fromIndex, int toIndex)
signal updateWidgetSettings(string section, int index, var settings)
signal moveWidget(string fromSection, int index, string toSection)
signal dragPotentialStarted
signal dragPotentialEnded
@@ -125,6 +126,7 @@ NBox {
Repeater {
model: widgetModel
delegate: Rectangle {
id: widgetItem
required property int index
@@ -158,6 +160,51 @@ NBox {
}
}
// Context menu for moving widget to other sections
NContextMenu {
id: contextMenu
parent: Overlay.overlay
width: 240 * scaling
model: [{
"label": "Move to left section",
"action": "left",
"icon": "arrow-bar-to-left",
"visible": root.sectionId !== "left"
}, {
"label": "Move to center section",
"action": "center",
"icon": "layout-columns",
"visible": root.sectionId !== "center"
}, {
"label": "Move to right section",
"action": "right",
"icon": "arrow-bar-to-right",
"visible": root.sectionId !== "right"
}]
onTriggered: action => root.moveWidget(root.sectionId, index, action)
}
// Update the MouseArea to use the new context menu
MouseArea {
id: contextMouseArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
z: -1 // Below the buttons but above background
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
// Check if click is not on the buttons area
const localX = mouse.x
const buttonsStartX = parent.width - (parent.buttonsCount * parent.buttonsWidth)
if (localX < buttonsStartX) {
// Use the helper function to open at mouse position
contextMenu.openAtItem(widgetItem, mouse.x, mouse.y)
}
}
}
}
RowLayout {
id: widgetContent
anchors.centerIn: parent
@@ -180,6 +227,7 @@ NBox {
active: BarWidgetRegistry.widgetHasUserSettings(modelData.id)
sourceComponent: NIconButton {
icon: "settings"
tooltipText: "Widget settings"
baseSize: miniButtonSize
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
@@ -220,6 +268,7 @@ NBox {
NIconButton {
icon: "close"
tooltipText: "Remove widget"
baseSize: miniButtonSize
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
@@ -9,12 +9,15 @@ import "./WidgetSettings" as WidgetSettings
// Widget Settings Dialog Component
Popup {
id: settingsPopup
// Don't replace by root!
id: widgetSettings
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
property bool isMasked: false
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
@@ -24,51 +27,34 @@ Popup {
padding: Style.marginXL * scaling
modal: true
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
// Load settings when popup opens with data
onOpened: {
// Mark this popup has opened in the PanelService
PanelService.willOpenPopup(widgetSettings)
// Load settings when popup opens with data
if (widgetData && widgetId) {
loadWidgetSettings()
}
}
function loadWidgetSettings() {
const widgetSettingsMap = {
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
"Battery": "WidgetSettings/BatterySettings.qml",
"Brightness": "WidgetSettings/BrightnessSettings.qml",
"Clock": "WidgetSettings/ClockSettings.qml",
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
"SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml",
"Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml"
}
const source = widgetSettingsMap[widgetId]
if (source) {
// Use setSource to pass properties at creation time
settingsLoader.setSource(source, {
"widgetData": widgetData,
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
})
}
onClosed: {
PanelService.willClosePopup(widgetSettings)
}
ColumnLayout {
background: Rectangle {
id: bgRect
opacity: widgetSettings.isMasked ? 0 : 1.0
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
}
contentItem: ColumnLayout {
id: content
opacity: widgetSettings.isMasked ? 0 : 1.0
width: parent.width
spacing: Style.marginM * scaling
@@ -77,7 +63,7 @@ Popup {
Layout.fillWidth: true
NText {
text: `${settingsPopup.widgetId} Settings`
text: `${widgetSettings.widgetId} Settings`
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
@@ -86,7 +72,7 @@ Popup {
NIconButton {
icon: "close"
onClicked: settingsPopup.close()
onClicked: widgetSettings.close()
}
}
@@ -117,7 +103,7 @@ Popup {
NButton {
text: "Cancel"
outlined: true
onClicked: settingsPopup.close()
onClicked: widgetSettings.close()
}
NButton {
@@ -126,11 +112,39 @@ Popup {
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings()
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
settingsPopup.close()
root.updateWidgetSettings(sectionId, widgetSettings.widgetIndex, newSettings)
widgetSettings.close()
}
}
}
}
}
function loadWidgetSettings() {
const widgetSettingsMap = {
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
"Battery": "WidgetSettings/BatterySettings.qml",
"Brightness": "WidgetSettings/BrightnessSettings.qml",
"Clock": "WidgetSettings/ClockSettings.qml",
"ControlCenter": "WidgetSettings/ControlCenterSettings.qml",
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
"Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml"
}
const source = widgetSettingsMap[widgetId]
if (source) {
// Use setSource to pass properties at creation time
settingsLoader.setSource(source, {
"widgetData": widgetData,
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
})
}
}
}
@@ -84,7 +84,7 @@ ColumnLayout {
NHeader {
label: "Clock display"
description: "Arrange your clock's layout. Click a token below to add it to the selected field."
description: "Customize your clock's display by adding tokens from the list below. To use the 12-hour format, you must include the 'AP' token."
}
RowLayout {
@@ -38,14 +38,6 @@ ColumnLayout {
}
}
NFilePicker {
id: filePicker
title: "Select a custom icon"
onFileSelected: function (filePath) {
valueCustomIconPath = "file://" + filePath
}
}
RowLayout {
spacing: Style.marginM * scaling
@@ -65,10 +57,13 @@ ColumnLayout {
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: valueIcon
font.pointSize: Style.fontSizeXL * scaling
font.pointSize: Style.fontSizeXXL * 1.5 * scaling
visible: valueIcon !== "" && valueCustomIconPath === ""
}
}
RowLayout {
spacing: Style.marginM * scaling
NButton {
enabled: !valueUseDistroLogo
text: "Browse Library"
@@ -85,9 +80,15 @@ ColumnLayout {
NIconPicker {
id: iconPicker
initialIcon: valueIcon
onIconSelected: function (iconName) {
valueIcon = iconName
valueCustomIconPath = ""
}
onIconSelected: iconName => {
valueIcon = iconName
valueCustomIconPath = ""
}
}
}
NFilePicker {
id: filePicker
title: "Select a custom icon"
onAccepted: paths => valueCustomIconPath = paths[0]
}
}
@@ -3,7 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Modules.SettingsPanel.Tabs as Tabs
import qs.Modules.Settings.Tabs as Tabs
import qs.Commons
import qs.Services
import qs.Widgets
@@ -21,7 +21,7 @@ NPanel {
panelKeyboardFocus: true
draggable: true
draggable: !PanelService.hasOpenedPopup
// Tabs enumeration, order is NOT relevant
enum Tab {
@@ -5,7 +5,7 @@ import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.SettingsPanel.Bar
import qs.Modules.Settings.Bar
ColumnLayout {
id: root
@@ -219,6 +219,7 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
@@ -233,6 +234,7 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
@@ -247,6 +249,7 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
@@ -341,6 +344,25 @@ ColumnLayout {
//Logger.log("BarTab", `Updated widget settings for ${settings.id} in ${section} section`)
}
function _moveWidgetBetweenSections(fromSection, index, toSection) {
// Get the widget from the source section
if (index >= 0 && index < Settings.data.bar.widgets[fromSection].length) {
var widget = Settings.data.bar.widgets[fromSection][index]
// Remove from source section
var sourceArray = Settings.data.bar.widgets[fromSection].slice()
sourceArray.splice(index, 1)
Settings.data.bar.widgets[fromSection] = sourceArray
// Add to target section
var targetArray = Settings.data.bar.widgets[toSection].slice()
targetArray.push(widget)
Settings.data.bar.widgets[toSection] = targetArray
//Logger.log("BarTab", `Moved widget ${widget.id} from ${fromSection} to ${toSection}`)
}
}
// Base list model for all combo boxes
ListModel {
id: availableWidgets
@@ -30,7 +30,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignTop
}
NInputButton {
NTextInputButton {
label: `${Quickshell.env("USER") || "user"}'s profile picture`
description: "Your profile picture that appears throughout the interface."
text: Settings.data.general.avatarImage
@@ -39,18 +39,20 @@ ColumnLayout {
buttonTooltip: "Browse for avatar image"
onInputEditingFinished: Settings.data.general.avatarImage = text
onButtonClicked: {
FilePickerService.open({
"title": "Select Avatar Image",
"initialPath": Settings.data.general.avatarImage || Quickshell.env("HOME"),
"selectFiles": true,
"scaling": scaling,
"parent": root,
"onSelected": path => Settings.data.general.avatarImage = path
})
filePicker.open()
}
}
}
NFilePicker {
id: filePicker
pickerType: "file"
title: "Select avatar image"
initialPath: Settings.data.general.avatarImage.substr(0, Settings.data.general.avatarImage.lastIndexOf("/")) || Quickshell.env("HOME")
nameFilters: ["Image files (*.jpg *.jpeg *.png *.gif *.pnm *.bmp *.face)", "All files (*)"]
onAccepted: paths => Settings.data.general.avatarImage = paths[0]
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
@@ -48,7 +48,7 @@ ColumnLayout {
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 12 * scaling
Layout.bottomMargin: Style.marginM * scaling
}
}
@@ -39,6 +39,13 @@ ColumnLayout {
onToggled: checked => Settings.data.notifications.doNotDisturb = checked
}
NToggle {
label: "Enable on screen display"
description: "Show volume and brightness changes in real-time."
checked: Settings.data.notifications.enableOSD
onToggled: checked => Settings.data.notifications.enableOSD = checked
}
NComboBox {
label: "Location"
description: "Where notifications appear on screen."
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
@@ -20,24 +21,15 @@ ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NInputButton {
NTextInputButton {
label: "Output folder"
description: "Folder where screen recordings will be saved."
placeholderText: "/home/xxx/Videos"
placeholderText: Quickshell.env("HOME") + "/Videos"
text: Settings.data.screenRecorder.directory
buttonIcon: "folder-open"
buttonTooltip: "Browse for output folder"
onInputEditingFinished: Settings.data.screenRecorder.directory = text
onButtonClicked: {
FilePickerService.open({
"title": "Select Output Folder",
"initialPath": Settings.data.screenRecorder.directory || Quickshell.env("HOME") + "/Videos",
"selectFiles": false,
"scaling": scaling,
"parent": root,
"onSelected": path => Settings.data.screenRecorder.directory = path
})
}
onButtonClicked: folderPicker.open()
}
ColumnLayout {
@@ -261,4 +253,12 @@ ColumnLayout {
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
NFilePicker {
id: folderPicker
pickerType: "folder"
title: "Select output folder"
initialPath: Settings.data.screenRecorder.directory || Quickshell.env("HOME") + "/Videos"
onAccepted: paths => Settings.data.screenRecorder.directory = paths[0]
}
}
@@ -11,6 +11,8 @@ ColumnLayout {
id: root
spacing: Style.marginL * scaling
property string specificFolderMonitorName: ""
NHeader {
label: "Wallpaper settings"
description: "Control how wallpapers are managed and displayed."
@@ -18,7 +20,7 @@ ColumnLayout {
NToggle {
label: "Enable wallpaper management"
description: "Manage wallpapers with Noctalia. (Uncheck if you prefer using another application)."
description: "Manage wallpapers with Noctalia. Uncheck if you prefer using another application."
checked: Settings.data.wallpaper.enabled
onToggled: checked => Settings.data.wallpaper.enabled = checked
Layout.bottomMargin: Style.marginL * scaling
@@ -29,7 +31,7 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NInputButton {
NTextInputButton {
id: wallpaperPathInput
label: "Wallpaper folder"
description: "Path to your main wallpaper folder."
@@ -37,13 +39,8 @@ ColumnLayout {
buttonIcon: "folder-open"
buttonTooltip: "Browse for wallpaper folder"
Layout.fillWidth: true
onInputEditingFinished: {
Settings.data.wallpaper.directory = text
}
onButtonClicked: {
openFileManager()
}
onInputEditingFinished: Settings.data.wallpaper.directory = text
onButtonClicked: mainFolderPicker.open()
}
// Monitor-specific directories
@@ -83,17 +80,15 @@ ColumnLayout {
font.pointSize: Style.fontSizeM * scaling
}
NInputButton {
NTextInputButton {
text: WallpaperService.getMonitorDirectory(modelData.name)
buttonIcon: "folder-open"
buttonTooltip: "Browse for wallpaper folder"
Layout.fillWidth: true
onInputEditingFinished: {
WallpaperService.setMonitorDirectory(modelData.name, text)
}
onInputEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, text)
onButtonClicked: {
openMonitorFileManager(modelData.name)
specificFolderMonitorName = modelData.name
monitorFolderPicker.open()
}
}
}
@@ -343,26 +338,17 @@ ColumnLayout {
Layout.bottomMargin: Style.marginXL * scaling
}
// File manager functions
function openFileManager() {
FilePickerService.open({
"title": "Select Wallpaper Folder",
"initialPath": Settings.data.wallpaper.directory || Quickshell.env("HOME"),
"selectFiles": false,
"scaling": scaling,
"parent": root,
"onSelected": path => Settings.data.wallpaper.directory = path
})
NFilePicker {
id: mainFolderPicker
pickerType: "folder"
title: "Select wallpaper folder"
onAccepted: paths => Settings.data.wallpaper.directory = paths[0]
}
function openMonitorFileManager(monitorName) {
FilePickerService.open({
"title": "Select Monitor Wallpaper Folder",
"initialPath": WallpaperService.getMonitorDirectory(monitorName),
"selectFiles": false,
"scaling": scaling,
"parent": root,
"onSelected": path => WallpaperService.setMonitorDirectory(monitorName, path)
})
NFilePicker {
id: monitorFolderPicker
pickerType: "folder"
title: "Select monitor wallpaper folder"
onAccepted: paths => WallpaperService.setMonitorDirectory(specificFolderMonitorName, paths[0])
}
}
@@ -18,7 +18,8 @@ NPanel {
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
draggable: true
draggable: !PanelService.hasOpenedPopup
panelContent: Rectangle {
id: wallpaperPanel
+24
View File
@@ -1,11 +1,32 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Commons
Singleton {
id: root
property bool hasAudioVisualizer: false
// Simple timer that run once when the widget structure has changed
// and determine if any MediaMini widget has the visualizer on
Timer {
id: timerCheckVisualizer
interval: 100
repeat: false
onTriggered: {
hasAudioVisualizer = false
const widgets = getAllWidgetInstances("MediaMini")
for (var i = 0; i < widgets.length; i++) {
const widget = widgets[i]
if (widget.showVisualizer) {
hasAudioVisualizer = true
}
}
}
}
// Registry to store actual widget instances
// Key format: "screenName|section|widgetId|index"
property var widgetInstances: ({})
@@ -20,6 +41,9 @@ Singleton {
"index": index,
"instance": instance
}
timerCheckVisualizer.restart()
Logger.log("BarService", "Registered widget:", key)
}
+13 -13
View File
@@ -15,8 +15,9 @@ Singleton {
"Bluetooth": bluetoothComponent,
"Brightness": brightnessComponent,
"Clock": clockComponent,
"ControlCenter": controlCenterComponent,
"CustomButton": customButtonComponent,
"DarkModeToggle": darkModeToggle,
"DarkMode": darkMode,
"KeepAwake": keepAwakeComponent,
"KeyboardLayout": keyboardLayoutComponent,
"MediaMini": mediaMiniComponent,
@@ -24,9 +25,8 @@ Singleton {
"NightLight": nightLightComponent,
"NotificationHistory": notificationHistoryComponent,
"PowerProfile": powerProfileComponent,
"PowerToggle": powerToggleComponent,
"ScreenRecorderIndicator": screenRecorderIndicatorComponent,
"SidePanelToggle": sidePanelToggleComponent,
"ScreenRecorder": screenRecorderComponent,
"SessionMenu": sessionMenuComponent,
"Spacer": spacerComponent,
"SystemMonitor": systemMonitorComponent,
"Taskbar": taskbarComponent,
@@ -100,7 +100,7 @@ Singleton {
"showVisualizer": false,
"visualizerType": "linear"
},
"SidePanelToggle": {
"ControlCenter": {
"allowUserSettings": true,
"useDistroLogo": false,
"icon": "noctalia",
@@ -135,8 +135,8 @@ Singleton {
property Component customButtonComponent: Component {
CustomButton {}
}
property Component darkModeToggle: Component {
DarkModeToggle {}
property Component darkMode: Component {
DarkMode {}
}
property Component keyboardLayoutComponent: Component {
KeyboardLayout {}
@@ -159,14 +159,14 @@ Singleton {
property Component powerProfileComponent: Component {
PowerProfile {}
}
property Component powerToggleComponent: Component {
PowerToggle {}
property Component sessionMenuComponent: Component {
SessionMenu {}
}
property Component screenRecorderIndicatorComponent: Component {
ScreenRecorderIndicator {}
property Component screenRecorderComponent: Component {
ScreenRecorder {}
}
property Component sidePanelToggleComponent: Component {
SidePanelToggle {}
property Component controlCenterComponent: Component {
ControlCenter {}
}
property Component spacerComponent: Component {
Spacer {}
+5 -2
View File
@@ -8,9 +8,9 @@ import qs.Commons
Singleton {
id: root
property bool shouldRun: BarService.hasAudioVisualizer || (PanelService.getPanel("controlCenterPanel") === PanelService.openedPanel) || PanelService.lockScreen.active
property var values: Array(barsCount).fill(0)
property int barsCount: 24
property var config: ({
"general": {
"bars": barsCount,
@@ -37,8 +37,11 @@ Singleton {
Process {
id: process
stdinEnabled: true
running: MediaService.isPlaying
running: root.shouldRun
command: ["cava", "-p", "/dev/stdin"]
onRunningChanged: {
Logger.log("Cava", "Process running:", running)
}
onExited: {
stdinEnabled = true
values = Array(barsCount).fill(0)
+2 -2
View File
@@ -114,9 +114,9 @@ Singleton {
// Get window title for focused window
function getFocusedWindowTitle() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
return windows[focusedWindowIndex].title || "(Unnamed window)"
return windows[focusedWindowIndex].title || ""
}
return "(No active window)"
return ""
}
// Generic workspace switching
-47
View File
@@ -1,47 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
QtObject {
id: root
// Function to open a file manager dialog
function open(options) {
var component = Qt.createComponent(Qt.resolvedUrl(Quickshell.shellDir + "/Widgets/NFilePicker.qml"))
if (component.status === Component.Ready) {
// Extract directory from file path if it's a file
var initialPath = options.initialPath || Quickshell.env("HOME")
if (options.selectFiles && initialPath !== Quickshell.env("HOME")) {
// If selecting files and path is not home, extract directory
var pathParts = initialPath.split('/')
if (pathParts.length > 1) {
pathParts.pop() // Remove filename
initialPath = pathParts.join('/') || '/'
}
}
var dialog = component.createObject(options.parent || Overlay.overlay, {
"title": options.title || "Select File/Folder",
"initialPath": initialPath,
"selectFiles": options.selectFiles || false,
"selectFolders": !options.selectFiles || false,
"scaling": options.scaling || 1.0
})
if (dialog) {
if (options.onSelected) {
if (options.selectFiles) {
dialog.fileSelected.connect(options.onSelected)
} else {
dialog.folderSelected.connect(options.onSelected)
}
}
dialog.open()
return dialog
}
} else {
console.error("Component error:", component.errorString())
}
return null
}
}
@@ -109,18 +109,35 @@ Item {
}
}
// TODO: delete in next major release
IpcHandler {
target: "powerPanel"
function toggle() {
powerPanel.toggle()
sessionMenuPanel.toggle()
ToastService.showWarning("IPC", "PowerPanel has been renamed to SessionMenu, this IPC call will be deprecated soon. Please use \"ipc call sessionMenu toggle\" instead.", 8000)
}
}
IpcHandler {
target: "sessionMenu"
function toggle() {
sessionMenuPanel.toggle()
}
}
// TODO: delete in next major release
IpcHandler {
target: "sidePanel"
function toggle() {
// Will attempt to open the panel next to the bar button if any.
sidePanel.toggle(BarService.lookupWidget("SidePanelToggle"))
controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter"))
ToastService.showWarning("IPC", "SidePanel has been renamed to ControlCenter, this IPC call will be deprecated soon. Please use \"ipc call controlCenter toggle\" instead.", 8000)
}
}
IpcHandler {
target: "controlCenter"
function toggle() {
// Will attempt to open the panel next to the bar button if any.
controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter"))
}
}
@@ -129,7 +146,7 @@ Item {
target: "wallpaper"
function toggle() {
if (Settings.data.wallpaper.enabled) {
wallpaperSelector.toggle()
wallpaperPanel.toggle()
}
}
+1 -1
View File
@@ -320,7 +320,7 @@ Singleton {
return ""
if (icon.startsWith("/") || icon.startsWith("file://"))
return icon
return AppIcons.iconFromName(icon)
return ThemeIcons.iconFromName(icon)
}
function stripTags(text) {
+25 -5
View File
@@ -10,15 +10,19 @@ Singleton {
// This is not a panel...
property var lockScreen: null
// Currently opened panel
property var openedPanel: null
readonly property bool hasOpenedPanel: (openedPanel !== null)
// Panels
property var registeredPanels: ({})
property var openedPanel: null
property bool hasOpenedPanel: false
signal willOpen
signal willClose
// Currently opened popups, can have more than one.
// ex: when opening an NIconPicker from a widget setting.
property var openedPopups: []
property bool hasOpenedPopup: false
signal popupChanged
// Register this panel
function registerPanel(panel) {
registeredPanels[panel.objectName] = panel
@@ -41,12 +45,15 @@ Singleton {
openedPanel.close()
}
openedPanel = panel
hasOpenedPanel = true
// emit signal
willOpen()
}
function willClosePanel(panel) {
hasOpenedPanel = false
// emit signal
willClose()
}
@@ -56,4 +63,17 @@ Singleton {
openedPanel = null
}
}
// Popups
function willOpenPopup(popup) {
openedPopups.push(popup)
hasOpenedPopup = (openedPopups.length !== 0)
popupChanged()
}
function willClosePopup(popup) {
openedPopups = openedPopups.filter(p => p !== popup)
hasOpenedPopup = (openedPopups.length !== 0)
popupChanged()
}
}
+53 -1
View File
@@ -12,6 +12,8 @@ Singleton {
readonly property var settings: Settings.data.screenRecorder
property bool isRecording: false
property bool isPending: false
// True only if the recorder actually started capturing at least once
property bool hasActiveRecording: false
property string outputPath: ""
property bool isAvailable: ProgramCheckerService.gpuScreenRecorderAvailable
@@ -36,7 +38,16 @@ Singleton {
return
}
isPending = true
hasActiveRecording = false
// First, ensure xdg-desktop-portal and a compositor portal are running
portalCheckProcess.exec({
"command": ["sh", "-c", // require core portal AND one of the backends
"pidof xdg-desktop-portal >/dev/null 2>&1 && (pidof xdg-desktop-portal-wlr >/dev/null 2>&1 || pidof xdg-desktop-portal-hyprland >/dev/null 2>&1 || pidof xdg-desktop-portal-gnome >/dev/null 2>&1 || pidof xdg-desktop-portal-kde >/dev/null 2>&1)"]
})
}
function launchRecorder() {
var filename = Time.getFormattedTimestamp() + ".mp4"
var videoDir = settings.directory
if (videoDir && !videoDir.endsWith("/")) {
@@ -56,7 +67,7 @@ Singleton {
notify-send "gpu-screen-recorder not installed!" -u critical
fi`
// Use Process instead of execDetached so we can monitor it
// Use Process instead of execDetached so we can monitor it and read stderr
recorderProcess.exec({
"command": ["sh", "-c", command]
})
@@ -71,12 +82,15 @@ Singleton {
return
}
ToastService.showNotice("Stopping recording…", outputPath, 2000)
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder' || pkill -SIGINT -f 'com.dec05eba.gpu_screen_recorder'"])
isRecording = false
isPending = false
pendingTimer.running = false
monitorTimer.running = false
hasActiveRecording = false
// Just in case, force kill after 3 seconds
killTimer.running = true
@@ -85,15 +99,50 @@ Singleton {
// Process to run and monitor gpu-screen-recorder
Process {
id: recorderProcess
stdout: StdioCollector {}
stderr: StdioCollector {}
onExited: function (exitCode, exitStatus) {
if (isPending) {
// Process ended while we were pending - likely cancelled or error
isPending = false
pendingTimer.running = false
// If it failed to start, show a clear error toast with stderr
if (exitCode !== 0) {
const err = String(stderr.text || "").trim()
if (err.length > 0)
ToastService.showError("Failed to start recording", err, 7000)
else
ToastService.showError("Failed to start recording", "gpu-screen-recorder exited unexpectedly.", 7000)
}
} else if (isRecording) {
// Process ended normally while recording
isRecording = false
monitorTimer.running = false
// Consider successful save if exitCode == 0
if (exitCode === 0) {
ToastService.showNotice("Recording saved", outputPath, 5000)
} else {
const err2 = String(stderr.text || "").trim()
if (err2.length > 0)
ToastService.showError("Recording failed", err2, 7000)
else
ToastService.showError("Recording failed", "The recorder exited with an error.", 7000)
}
}
}
}
// Pre-flight check for xdg-desktop-portal
Process {
id: portalCheckProcess
onExited: function (exitCode, exitStatus) {
if (exitCode === 0) {
// Portals available, proceed to launch
launchRecorder()
} else {
isPending = false
hasActiveRecording = false
ToastService.showError("Desktop portals not running", "Start xdg-desktop-portal and a compositor portal (wlr/hyprland/gnome/kde).", 8000)
}
}
}
@@ -108,7 +157,10 @@ Singleton {
// Process is still running after 2 seconds - assume recording started successfully
isPending = false
isRecording = true
hasActiveRecording = true
monitorTimer.running = true
// Don't show a toast when recording starts to avoid having the toast in every video.
//ToastService.showNotice("Recording started", outputPath, 4000)
} else if (isPending) {
// Process not running anymore - was cancelled or failed
isPending = false
+11 -23
View File
@@ -13,7 +13,6 @@ Rectangle {
property color backgroundColor: Color.mPrimary
property color textColor: Color.mOnPrimary
property color hoverColor: Color.mTertiary
property color pressColor: Color.mSecondary
property bool enabled: true
property real fontSize: Style.fontSizeM * scaling
property int fontWeight: Style.fontWeightBold
@@ -38,8 +37,6 @@ Rectangle {
color: {
if (!enabled)
return outlined ? Color.transparent : Qt.lighter(Color.mSurfaceVariant, 1.2)
if (pressed)
return pressColor
if (hovered)
return hoverColor
return outlined ? Color.transparent : backgroundColor
@@ -113,7 +110,7 @@ Rectangle {
if (!root.enabled)
return Color.mOnSurfaceVariant
if (root.outlined) {
if (root.pressed || root.hovered)
if (root.hovered)
return root.textColor
return root.backgroundColor
}
@@ -153,33 +150,24 @@ Rectangle {
}
onExited: {
root.hovered = false
root.pressed = false
if (tooltipText) {
tooltip.hide()
}
}
onPressed: mouse => {
root.pressed = true
if (tooltipText) {
tooltip.hide()
}
if (mouse.button === Qt.LeftButton) {
root.clicked()
} else if (mouse.button == Qt.RightButton) {
root.rightClicked()
} else if (mouse.button == Qt.MiddleButton) {
root.middleClicked()
}
}
onReleased: mouse => {
root.pressed = false
if (tooltipText) {
tooltip.hide()
}
if (!root.hovered) {
return
}
if (mouse.button === Qt.LeftButton) {
root.clicked()
} else if (mouse.button == Qt.RightButton) {
root.rightClicked()
} else if (mouse.button == Qt.MiddleButton) {
root.middleClicked
}
}
onCanceled: {
root.pressed = false
root.hovered = false
if (tooltipText) {
tooltip.hide()
+10 -3
View File
@@ -26,7 +26,14 @@ Popup {
y: (parent.height - height) * 0.5
modal: true
clip: true
onOpened: {
PanelService.willOpenPopup(root)
}
onClosed: {
PanelService.willClosePopup(root)
}
function rgbToHsv(r, g, b) {
r /= 255
@@ -110,9 +117,9 @@ Popup {
border.width: Math.max(1, Style.borderM * scaling)
}
NScrollView {
contentItem: NScrollView {
id: scrollView
anchors.fill: parent
width: parent.width
verticalPolicy: ScrollBar.AlwaysOff
horizontalPolicy: ScrollBar.AlwaysOff
+24 -17
View File
@@ -85,16 +85,23 @@ RowLayout {
popup: Popup {
y: combo.height
width: combo.width
implicitWidth: combo.width - Style.marginM * scaling
implicitHeight: Math.min(root.popupHeight, contentItem.implicitHeight + Style.marginM * scaling * 2)
padding: Style.marginM * scaling
contentItem: ListView {
property var comboBoxRoot: root
clip: true
implicitHeight: contentHeight
onOpened: {
PanelService.willOpenPopup(root)
}
onClosed: {
PanelService.willClosePopup(root)
}
contentItem: NListView {
model: combo.popup.visible ? root.model : null
ScrollIndicator.vertical: ScrollIndicator {}
implicitHeight: contentHeight
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
delegate: ItemDelegate {
width: combo.width
@@ -108,17 +115,15 @@ RowLayout {
}
onClicked: {
ListView.view.comboBoxRoot.selected(ListView.view.comboBoxRoot.model.get(index).key)
root.selected(root.model.get(index).key)
combo.currentIndex = index
combo.popup.close()
}
contentItem: NText {
text: name
font.pointSize: Style.fontSizeM * scaling
color: highlighted ? Color.mSurface : Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
background: Rectangle {
width: combo.width - Style.marginM * scaling * 3
color: highlighted ? Color.mTertiary : Color.transparent
radius: Style.radiusS * scaling
Behavior on color {
ColorAnimation {
duration: Style.animationFast
@@ -126,10 +131,12 @@ RowLayout {
}
}
background: Rectangle {
width: combo.width - Style.marginM * scaling * 3
color: highlighted ? Color.mTertiary : Color.transparent
radius: Style.radiusS * scaling
contentItem: NText {
text: name
font.pointSize: Style.fontSizeM * scaling
color: highlighted ? Color.mOnTertiary : Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Behavior on color {
ColorAnimation {
duration: Style.animationFast
+113
View File
@@ -0,0 +1,113 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
Popup {
id: root
property alias model: listView.model
property real itemHeight: 36 * scaling
property real itemPadding: Style.marginM * scaling
signal triggered(string action)
width: 180 * scaling
padding: Style.marginS * scaling
onOpened: PanelService.willOpenPopup(root)
onClosed: PanelService.willClosePopup(root)
background: Rectangle {
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
}
contentItem: ListView {
id: listView
implicitHeight: contentHeight
spacing: Style.marginXXS * scaling
interactive: contentHeight > root.height
clip: true
delegate: ItemDelegate {
id: menuItem
width: listView.width
height: modelData.visible !== false ? root.itemHeight : 0
visible: modelData.visible !== false
opacity: modelData.enabled !== false ? 1.0 : 0.5
enabled: modelData.enabled !== false
// Store reference to the popup
property var popup: root
background: Rectangle {
color: menuItem.hovered && menuItem.enabled ? Color.mTertiary : Color.transparent
radius: Style.radiusS * scaling
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
contentItem: RowLayout {
spacing: Style.marginS * scaling
// Optional icon
NIcon {
visible: modelData.icon !== undefined
icon: modelData.icon || ""
font.pointSize: Style.fontSizeM * scaling
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
Layout.leftMargin: root.itemPadding
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
NText {
text: modelData.label || modelData.text || ""
font.pointSize: Style.fontSizeM * scaling
color: menuItem.hovered && menuItem.enabled ? Color.mOnTertiary : Color.mOnSurface
verticalAlignment: Text.AlignVCenter
Layout.fillWidth: true
Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
onClicked: {
if (enabled) {
popup.triggered(modelData.action || modelData.key || index.toString())
popup.close()
}
}
}
}
// Helper function to open at mouse position
function openAt(x, y) {
root.x = x
root.y = y
root.open()
}
// Helper function to open at item
function openAtItem(item, mouseX, mouseY) {
var pos = item.mapToItem(root.parent, mouseX || 0, mouseY || 0)
openAt(pos.x, pos.y)
}
}
+324 -331
View File
@@ -21,335 +21,312 @@ Rectangle {
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
// Scrollable list of tokens
NScrollView {
NListView {
id: tokensList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
model: ListModel {
ListView {
id: tokensList
model: ListModel {
// Common format combinations
ListElement {
category: "Common"
token: "h:mm AP"
description: "12-hour time with minutes"
example: "2:30 PM"
}
ListElement {
category: "Common"
token: "HH:mm"
description: "24-hour time with minutes"
example: "14:30"
}
ListElement {
category: "Common"
token: "HH:mm:ss"
description: "24-hour time with seconds"
example: "14:30:45"
}
ListElement {
category: "Common"
token: "ddd MMM d"
description: "Weekday, month and day"
example: "Mon Dec 25"
}
ListElement {
category: "Common"
token: "yyyy-MM-dd"
description: "ISO date format"
example: "2023-12-25"
}
ListElement {
category: "Common"
token: "MM/dd/yyyy"
description: "US date format"
example: "12/25/2023"
}
ListElement {
category: "Common"
token: "dd.MM.yyyy"
description: "European date format"
example: "25.12.2023"
}
ListElement {
category: "Common"
token: "ddd, MMM dd"
description: "Weekday with date"
example: "Fri, Dec 12"
}
// Common format combinations
ListElement {
category: "Common"
token: "h:mm AP"
description: "12-hour time with minutes"
example: "2:30 PM"
}
ListElement {
category: "Common"
token: "HH:mm"
description: "24-hour time with minutes"
example: "14:30"
}
ListElement {
category: "Common"
token: "HH:mm:ss"
description: "24-hour time with seconds"
example: "14:30:45"
}
ListElement {
category: "Common"
token: "ddd MMM d"
description: "Weekday, month and day"
example: "Mon Dec 25"
}
ListElement {
category: "Common"
token: "yyyy-MM-dd"
description: "ISO date format"
example: "2023-12-25"
}
ListElement {
category: "Common"
token: "MM/dd/yyyy"
description: "US date format"
example: "12/25/2023"
}
ListElement {
category: "Common"
token: "dd.MM.yyyy"
description: "European date format"
example: "25.12.2023"
}
ListElement {
category: "Common"
token: "ddd, MMM dd"
description: "Weekday with date"
example: "Fri, Dec 12"
}
// Hour tokens
// ListElement {
// category: "Hour"
// token: "h"
// description: "Hour without leading zero (12-hour when used with AP/ap, otherwise 24-hour)"
// example: "2 (needs AP/ap for 12hr)"
// }
// ListElement {
// category: "Hour"
// token: "hh"
// description: "Hour with leading zero (12-hour when used with AP/ap, otherwise 24-hour)"
// example: "02 (needs AP/ap for 12hr)"
// }
// ListElement {
// category: "Hour"
// token: "h AP"
// description: "12-hour format with AM/PM"
// example: "2 PM"
// }
// ListElement {
// category: "Hour"
// token: "hh AP"
// description: "12-hour format with leading zero and AM/PM"
// example: "02 PM"
// }
ListElement {
category: "Hour"
token: "H"
description: "Hour without leading zero (0-23) - 24-hour format"
example: "14"
}
ListElement {
category: "Hour"
token: "HH"
description: "Hour with leading zero (00-23) - 24-hour format"
example: "14"
}
// Hour tokens
// ListElement {
// category: "Hour"
// token: "h"
// description: "Hour without leading zero (12-hour when used with AP/ap, otherwise 24-hour)"
// example: "2 (needs AP/ap for 12hr)"
// }
// ListElement {
// category: "Hour"
// token: "hh"
// description: "Hour with leading zero (12-hour when used with AP/ap, otherwise 24-hour)"
// example: "02 (needs AP/ap for 12hr)"
// }
// ListElement {
// category: "Hour"
// token: "h AP"
// description: "12-hour format with AM/PM"
// example: "2 PM"
// }
// ListElement {
// category: "Hour"
// token: "hh AP"
// description: "12-hour format with leading zero and AM/PM"
// example: "02 PM"
// }
ListElement {
category: "Hour"
token: "H"
description: "Hour without leading zero (0-23) - 24-hour format"
example: "14"
}
ListElement {
category: "Hour"
token: "HH"
description: "Hour with leading zero (00-23) - 24-hour format"
example: "14"
}
// Minute tokens
ListElement {
category: "Minute"
token: "m"
description: "Minute without leading zero (0-59)"
example: "30"
}
ListElement {
category: "Minute"
token: "mm"
description: "Minute with leading zero (00-59)"
example: "30"
}
// Minute tokens
ListElement {
category: "Minute"
token: "m"
description: "Minute without leading zero (0-59)"
example: "30"
}
ListElement {
category: "Minute"
token: "mm"
description: "Minute with leading zero (00-59)"
example: "30"
}
// Second tokens
ListElement {
category: "Second"
token: "s"
description: "Second without leading zero (0-59)"
example: "45"
}
ListElement {
category: "Second"
token: "ss"
description: "Second with leading zero (00-59)"
example: "45"
}
// Second tokens
ListElement {
category: "Second"
token: "s"
description: "Second without leading zero (0-59)"
example: "45"
}
ListElement {
category: "Second"
token: "ss"
description: "Second with leading zero (00-59)"
example: "45"
}
// AM/PM tokens
ListElement {
category: "AM/PM"
token: "AP"
description: "AM/PM in uppercase"
example: "PM"
}
ListElement {
category: "AM/PM"
token: "ap"
description: "am/pm in lowercase"
example: "pm"
}
// AM/PM tokens
ListElement {
category: "AM/PM"
token: "AP"
description: "AM/PM in uppercase"
example: "PM"
}
ListElement {
category: "AM/PM"
token: "ap"
description: "am/pm in lowercase"
example: "pm"
}
// Timezone tokens
ListElement {
category: "Timezone"
token: "t"
description: "Timezone abbreviation"
example: "UTC"
}
// Timezone tokens
ListElement {
category: "Timezone"
token: "t"
description: "Timezone abbreviation"
example: "UTC"
}
// Year tokens
ListElement {
category: "Year"
token: "yy"
description: "Year as two-digit number (00-99)"
example: "23"
}
ListElement {
category: "Year"
token: "yyyy"
description: "Year as four-digit number"
example: "2023"
}
// Year tokens
ListElement {
category: "Year"
token: "yy"
description: "Year as two-digit number (00-99)"
example: "23"
}
ListElement {
category: "Year"
token: "yyyy"
description: "Year as four-digit number"
example: "2023"
}
// Month tokens
ListElement {
category: "Month"
token: "M"
description: "Month as number without leading zero (1-12)"
example: "12"
}
ListElement {
category: "Month"
token: "MM"
description: "Month as number with leading zero (01-12)"
example: "12"
}
ListElement {
category: "Month"
token: "MMM"
description: "Abbreviated month name"
example: "Dec"
}
ListElement {
category: "Month"
token: "MMMM"
description: "Full month name"
example: "December"
}
// Month tokens
ListElement {
category: "Month"
token: "M"
description: "Month as number without leading zero (1-12)"
example: "12"
}
ListElement {
category: "Month"
token: "MM"
description: "Month as number with leading zero (01-12)"
example: "12"
}
ListElement {
category: "Month"
token: "MMM"
description: "Abbreviated month name"
example: "Dec"
}
ListElement {
category: "Month"
token: "MMMM"
description: "Full month name"
example: "December"
}
// Day tokens
ListElement {
category: "Day"
token: "d"
description: "Day without leading zero (1-31)"
example: "25"
}
ListElement {
category: "Day"
token: "dd"
description: "Day with leading zero (01-31)"
example: "25"
}
ListElement {
category: "Day"
token: "ddd"
description: "Abbreviated day name"
example: "Mon"
}
ListElement {
category: "Day"
token: "dddd"
description: "Full day name"
example: "Monday"
}
}
// Day tokens
ListElement {
category: "Day"
token: "d"
description: "Day without leading zero (1-31)"
example: "25"
delegate: Rectangle {
id: tokenDelegate
width: tokensList.width
height: layout.implicitHeight + Style.marginS * scaling
radius: Style.radiusS * scaling
color: {
if (tokenMouseArea.containsMouse) {
return Qt.alpha(Color.mPrimary, 0.1)
}
ListElement {
category: "Day"
token: "dd"
description: "Day with leading zero (01-31)"
example: "25"
}
ListElement {
category: "Day"
token: "ddd"
description: "Abbreviated day name"
example: "Mon"
}
ListElement {
category: "Day"
token: "dddd"
description: "Full day name"
example: "Monday"
return index % 2 === 0 ? Color.mSurfaceVariant : Qt.alpha(Color.mSurfaceVariant, 0.6)
}
// Mouse area for the entire delegate
MouseArea {
id: tokenMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Emit the signal with the token
root.tokenClicked(model.token)
// Visual feedback
clickAnimation.start()
}
}
delegate: Rectangle {
id: tokenDelegate
width: tokensList.width
height: layout.implicitHeight + Style.marginS * scaling
radius: Style.radiusS * scaling
color: {
if (tokenMouseArea.containsMouse) {
return Qt.alpha(Color.mPrimary, 0.1)
}
return index % 2 === 0 ? Color.mSurfaceVariant : Qt.alpha(Color.mSurfaceVariant, 0.6)
// Click animation
SequentialAnimation {
id: clickAnimation
PropertyAnimation {
target: tokenDelegate
property: "color"
to: Qt.alpha(Color.mPrimary, 0.3)
duration: 100
}
// Mouse area for the entire delegate
MouseArea {
id: tokenMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Emit the signal with the token
root.tokenClicked(model.token)
// Visual feedback
clickAnimation.start()
}
PropertyAnimation {
target: tokenDelegate
property: "color"
to: tokenMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.1) : (index % 2 === 0 ? Color.mSurface : Color.mSurfaceVariant)
duration: 200
}
}
// Click animation
SequentialAnimation {
id: clickAnimation
PropertyAnimation {
target: tokenDelegate
property: "color"
to: Qt.alpha(Color.mPrimary, 0.3)
duration: 100
}
PropertyAnimation {
target: tokenDelegate
property: "color"
to: tokenMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.1) : (index % 2 === 0 ? Color.mSurface : Color.mSurfaceVariant)
duration: 200
}
}
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
spacing: Style.marginM * scaling
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
spacing: Style.marginM * scaling
// Category badge
Rectangle {
Layout.alignment: Qt.AlignVCenter
width: 70 * scaling
height: 22 * scaling
color: getCategoryColor(model.category)[0]
radius: Style.radiusS * scaling
opacity: tokenMouseArea.containsMouse ? 0.9 : 1.0
// Category badge
Rectangle {
Layout.alignment: Qt.AlignVCenter
width: 70 * scaling
height: 22 * scaling
color: getCategoryColor(model.category)[0]
radius: Style.radiusS * scaling
opacity: tokenMouseArea.containsMouse ? 0.9 : 1.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
NText {
anchors.centerIn: parent
text: model.category
color: getCategoryColor(model.category)[1]
font.pointSize: Style.fontSizeXS * scaling
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
// Token - Made more prominent and clickable
Rectangle {
id: tokenButton
Layout.alignment: Qt.AlignVCenter // Added this line
width: 100 * scaling
height: 22 * scaling
color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface
radius: Style.radiusS * scaling
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
anchors.centerIn: parent
text: model.token
color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurface
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
// Description
NText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter // Added this line
text: model.description
color: tokenMouseArea.containsMouse ? Color.mOnSurface : Color.mOnSurfaceVariant
anchors.centerIn: parent
text: model.category
color: getCategoryColor(model.category)[1]
font.pointSize: Style.fontSizeXS * scaling
}
}
// Token - Made more prominent and clickable
Rectangle {
id: tokenButton
Layout.alignment: Qt.AlignVCenter // Added this line
width: 100 * scaling
height: 22 * scaling
color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface
radius: Style.radiusS * scaling
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
anchors.centerIn: parent
text: model.token
color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurface
font.pointSize: Style.fontSizeS * scaling
wrapMode: Text.WordWrap
font.weight: Style.fontWeightBold
Behavior on color {
ColorAnimation {
@@ -357,41 +334,57 @@ Rectangle {
}
}
}
}
// Live example
Rectangle {
Layout.alignment: Qt.AlignVCenter // Added this line
width: 90 * scaling
height: 22 * scaling
color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant
radius: Style.radiusS * scaling
border.color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Description
NText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter // Added this line
text: model.description
color: tokenMouseArea.containsMouse ? Color.mOnSurface : Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
wrapMode: Text.WordWrap
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
// Live example
Rectangle {
Layout.alignment: Qt.AlignVCenter // Added this line
width: 90 * scaling
height: 22 * scaling
color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant
radius: Style.radiusS * scaling
border.color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
anchors.centerIn: parent
text: Qt.formatDateTime(root.sampleDate, model.token)
color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
anchors.centerIn: parent
text: Qt.formatDateTime(root.sampleDate, model.token)
color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
+171 -782
View File
@@ -1,816 +1,205 @@
import QtCore
import QtQuick
import QtQuick.Layouts
import QtQuick.Dialogs
import QtQuick.Controls
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import "../Helpers/FuzzySort.js" as FuzzySort
Popup {
Item {
id: root
// Properties
property string title: "File Picker"
property string initialPath: Quickshell.env("HOME") || "/home"
property bool selectFiles: true
property bool selectFolders: true
property var nameFilters: ["*"]
property bool showDirs: true
property real scaling: 1.0
// Public API Properties
property string initialPath: ""
property var selectedPaths: []
property string currentPath: initialPath
property bool shouldResetSelection: false
property string selectedPath: ""
property bool multipleSelection: false
property string pickerType: "file" // "file" or "folder"
property var nameFilters: ["All files (*)"] // e.g., ["Image files (*.png *.jpg)", "Text files (*.txt)"]
property string title: pickerType === "folder" ? "Select Folder" : "Select File"
property string acceptLabel: "Select"
property string rejectLabel: "Cancel"
// Signals
signal fileSelected(string path)
signal filesSelected(var paths)
signal folderSelected(string path)
signal cancelled
// State properties
property bool isOpen: false
function openFileManager() {
if (!root.currentPath)
root.currentPath = root.initialPath
shouldResetSelection = true
open()
}
// Signals for external connections
signal accepted(var paths)
signal rejected
signal pathSelected(string path)
signal pathsSelected(var paths)
signal beforeOpen
signal afterClose
function getFileIcon(fileName) {
const ext = fileName.split('.').pop().toLowerCase()
const iconMap = {
"txt": 'filepicker-file-text',
"md": 'filepicker-file-text',
"log": 'filepicker-file-text',
"jpg": 'filepicker-photo',
"jpeg": 'filepicker-photo',
"png": 'filepicker-photo',
"gif": 'filepicker-photo',
"bmp": 'filepicker-photo',
"svg": 'filepicker-photo',
"mp4": 'filepicker-video',
"avi": 'filepicker-video',
"mkv": 'filepicker-video',
"mov": 'filepicker-video',
"mp3": 'filepicker-music',
"wav": 'filepicker-music',
"flac": 'filepicker-music',
"ogg": 'filepicker-music',
"zip": 'filepicker-archive',
"tar": 'filepicker-archive',
"gz": 'filepicker-archive',
"rar": 'filepicker-archive',
"7z": 'filepicker-archive',
"pdf": 'filepicker-text',
"doc": 'filepicker-text',
"docx": 'filepicker-text',
"xls": 'filepicker-table',
"xlsx": 'filepicker-table',
"ppt": 'filepicker-presentation',
"pptx": 'filepicker-presentation',
"html": 'filepicker-code',
"htm": 'filepicker-code',
"css": 'filepicker-code',
"js": 'filepicker-code',
"json": 'filepicker-code',
"xml": 'filepicker-code',
"exe": 'filepicker-settings',
"app": 'filepicker-settings',
"deb": 'filepicker-settings',
"rpm": 'filepicker-settings'
// Public functions
function open() {
beforeOpen()
if (PanelService.openedPanel !== null) {
PanelService.openedPanel.isMasked = true
}
return iconMap[ext] || 'filepicker-file'
for (var i = 0; i < PanelService.openedPopups.length; i++) {
PanelService.openedPopups[i].isMasked = true
}
isOpen = true
// Small delay to ensure panel changes happen first
Qt.callLater(function () {
if (pickerType === "folder") {
folderDialog.open()
} else {
fileDialog.open()
}
})
}
function formatFileSize(bytes) {
if (bytes === 0)
return "0 B"
const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]
}
function confirmSelection() {
if (filePickerPanel.currentSelection.length === 0)
return
root.selectedPaths = filePickerPanel.currentSelection
const path = filePickerPanel.currentSelection[0]
if (filePickerPanel.currentSelection.length === 1) {
const isDir = folderModel.get(folderModel.indexOf(path), "fileIsDir")
if (root.selectFiles && !root.selectFolders)
root.fileSelected(path)
else if (root.selectFolders && !root.selectFiles)
root.folderSelected(path)
else
isDir ? root.folderSelected(path) : root.fileSelected(path)
function close() {
if (pickerType === "folder") {
folderDialog.close()
} else {
root.filesSelected(filePickerPanel.currentSelection)
fileDialog.close()
}
root.close()
handleClose()
}
function updateFilteredModel() {
filteredModel.clear()
const searchText = filePickerPanel.filterText.toLowerCase()
function handleClose() {
isOpen = false
for (var i = 0; i < folderModel.count; i++) {
const fileName = folderModel.get(i, "fileName")
const filePath = folderModel.get(i, "filePath")
const fileIsDir = folderModel.get(i, "fileIsDir")
const fileSize = folderModel.get(i, "fileSize")
if (PanelService.openedPanel !== null) {
PanelService.openedPanel.isMasked = false
}
if (root.selectFolders && !root.selectFiles && !fileIsDir)
continue
if (searchText === "" || fileName.toLowerCase().includes(searchText)) {
filteredModel.append({
"fileName": fileName,
"filePath": filePath,
"fileIsDir": fileIsDir,
"fileSize": fileSize
})
for (var i = 0; i < PanelService.openedPopups.length; i++) {
PanelService.openedPopups[i].isMasked = false
}
afterClose()
}
function reset() {
selectedPaths = []
selectedPath = ""
}
// Helper function to set file extensions easily
function setFileExtensions(extensions) {
if (!extensions || extensions.length === 0) {
nameFilters = ["All files (*)"]
return
}
var filters = []
for (var i = 0; i < extensions.length; i++) {
var ext = extensions[i]
if (typeof ext === "string") {
// Simple extension like "png"
filters.push(ext.toUpperCase() + " files (*." + ext + ")")
} else if (typeof ext === "object" && ext.label && ext.extensions) {
// Complex filter like {label: "Images", extensions: ["png", "jpg", "jpeg"]}
var filterStr = ext.label + " ("
for (var j = 0; j < ext.extensions.length; j++) {
filterStr += "*." + ext.extensions[j]
if (j < ext.extensions.length - 1)
filterStr += " "
}
filterStr += ")"
filters.push(filterStr)
}
}
if (filters.length > 0) {
filters.push("All files (*)")
nameFilters = filters
}
}
width: 900 * scaling
height: 700 * scaling
modal: true
closePolicy: Popup.CloseOnEscape
anchors.centerIn: Overlay.overlay
background: Rectangle {
color: Color.mSurfaceVariant
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Helper function to convert URL to local path
function urlToPath(url) {
var path = url.toString()
// Remove file:// prefix (works for both Windows and Unix)
path = path.replace(/^file:\/\/\//, "/") // Unix
path = path.replace(/^file:\/\//, "") // Windows
// Handle Windows drive letters
if (Qt.platform.os === "windows") {
path = path.replace(/^\/([A-Z]:)/, "$1")
}
return path
}
Rectangle {
id: filePickerPanel
anchors.fill: parent
anchors.margins: Style.marginL * scaling
color: Color.transparent
property string filterText: ""
property var currentSelection: []
property bool viewMode: true // true = grid, false = list
property string searchText: ""
property bool showSearchBar: false
focus: true
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) {
filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar
if (filePickerPanel.showSearchBar)
Qt.callLater(() => searchInput.forceActiveFocus())
event.accepted = true
} else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) {
filePickerPanel.showSearchBar = false
filePickerPanel.searchText = ""
filePickerPanel.filterText = ""
root.updateFilteredModel()
event.accepted = true
}
}
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM * scaling
// Header
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
icon: "filepicker-folder"
color: Color.mPrimary
font.pointSize: Style.fontSizeXXL * scaling
}
NText {
text: root.title
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "filepicker-refresh"
tooltipText: "Refresh"
onClicked: folderModel.refresh()
}
NIconButton {
icon: "filepicker-close"
tooltipText: "Close"
onClicked: {
root.cancelled()
root.close()
}
}
}
NDivider {
Layout.fillWidth: true
}
// Navigation toolbar
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 45 * scaling
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
NIconButton {
icon: "filepicker-arrow-up"
tooltipText: "Up"
baseSize: Style.baseWidgetSize * 0.8
enabled: folderModel.folder.toString() !== "file:///"
onClicked: {
const parentPath = folderModel.parentFolder.toString().replace("file://", "")
folderModel.folder = "file://" + parentPath
root.currentPath = parentPath
}
}
NIconButton {
icon: "filepicker-home"
tooltipText: "Home"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
const homePath = Quickshell.env("HOME") || "/home"
folderModel.folder = "file://" + homePath
root.currentPath = homePath
}
}
NIconButton {
icon: filePickerPanel.viewMode ? "filepicker-list" : "filepicker-layout-grid"
tooltipText: filePickerPanel.viewMode ? "List View" : "Grid View"
baseSize: Style.baseWidgetSize * 0.8
onClicked: filePickerPanel.viewMode = !filePickerPanel.viewMode
}
NIconButton {
icon: filePickerPanel.showSearchBar ? "filepicker-x" : "filepicker-search"
tooltipText: filePickerPanel.showSearchBar ? "Close Search" : "Search"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar
if (!filePickerPanel.showSearchBar) {
filePickerPanel.searchText = ""
filePickerPanel.filterText = ""
root.updateFilteredModel()
}
}
}
NTextInput {
id: locationInput
text: root.currentPath
placeholderText: "Enter path..."
Layout.fillWidth: true
onEditingFinished: {
const newPath = text.trim()
if (newPath !== "" && newPath !== root.currentPath) {
folderModel.folder = "file://" + newPath
root.currentPath = newPath
} else {
text = root.currentPath
}
}
Connections {
target: root
function onCurrentPathChanged() {
if (!locationInput.activeFocus)
locationInput.text = root.currentPath
}
}
}
}
}
// Search bar
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 45 * scaling
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: filePickerPanel.showSearchBar
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
NIcon {
icon: "filepicker-search"
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
}
NTextInput {
id: searchInput
placeholderText: "Search files and folders..."
Layout.fillWidth: true
text: filePickerPanel.searchText
onTextChanged: {
filePickerPanel.searchText = text
filePickerPanel.filterText = text
root.updateFilteredModel()
}
Keys.onEscapePressed: {
filePickerPanel.showSearchBar = false
filePickerPanel.searchText = ""
filePickerPanel.filterText = ""
root.updateFilteredModel()
}
}
NIconButton {
icon: "filepicker-x"
tooltipText: "Clear"
baseSize: Style.baseWidgetSize * 0.6
visible: filePickerPanel.searchText.length > 0
onClicked: {
searchInput.text = ""
filePickerPanel.searchText = ""
filePickerPanel.filterText = ""
root.updateFilteredModel()
}
}
}
}
// File list area
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurface
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
clip: true
FolderListModel {
id: folderModel
folder: "file://" + root.currentPath
nameFilters: root.nameFilters
showDirs: root.showDirs
showHidden: true
sortField: FolderListModel.Name
sortReversed: false
onFolderChanged: {
root.currentPath = folder.toString().replace("file://", "")
filePickerPanel.currentSelection = []
}
onStatusChanged: {
if (status === FolderListModel.Error) {
if (root.currentPath !== Quickshell.env("HOME")) {
folder = "file://" + Quickshell.env("HOME")
root.currentPath = Quickshell.env("HOME")
}
} else if (status === FolderListModel.Ready) {
root.updateFilteredModel()
}
}
}
ListModel {
id: filteredModel
}
// Common scroll bar component
Component {
id: scrollBarComponent
ScrollBar {
policy: ScrollBar.AsNeeded
contentItem: Rectangle {
implicitWidth: 6 * scaling
implicitHeight: 100
radius: Style.radiusM * scaling
color: parent.pressed ? Qt.alpha(Color.mTertiary, 0.8) : parent.hovered ? Qt.alpha(Color.mTertiary, 0.8) : Qt.alpha(Color.mTertiary, 0.8)
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: 6 * scaling
implicitHeight: 100
color: Color.transparent
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
radius: (Style.radiusM * scaling) / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}
// Grid view
GridView {
id: gridView
anchors.fill: parent
anchors.margins: Style.marginM * scaling
model: filteredModel
visible: filePickerPanel.viewMode
clip: true
property int columns: Math.max(1, Math.floor(width / (120 * scaling)))
property int itemSize: Math.floor((width - leftMargin - rightMargin - (columns * Style.marginS * scaling)) / columns)
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.8) + Style.marginXS * scaling + Style.fontSizeS * scaling + Style.marginM * scaling
leftMargin: Style.marginS * scaling
rightMargin: Style.marginS * scaling
topMargin: Style.marginS * scaling
bottomMargin: Style.marginS * scaling
ScrollBar.vertical: scrollBarComponent.createObject(gridView, {
"parent": gridView,
"x": gridView.mirrored ? 0 : gridView.width - width,
"y": 0,
"height": gridView.height
})
delegate: Rectangle {
id: gridItem
width: gridView.itemSize
height: gridView.cellHeight
color: Color.transparent
radius: Style.radiusM * scaling
property bool isSelected: filePickerPanel.currentSelection.includes(model.filePath)
Rectangle {
anchors.fill: parent
color: Color.transparent
radius: parent.radius
border.color: isSelected ? Color.mSecondary : Color.mSurface
border.width: Math.max(1, Style.borderL * scaling)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
Rectangle {
anchors.fill: parent
color: (mouseArea.containsMouse && !isSelected) ? Color.mTertiary : Color.transparent
radius: parent.radius
border.color: (mouseArea.containsMouse && !isSelected) ? Color.mTertiary : Color.transparent
border.width: Math.max(1, Style.borderS * scaling)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * scaling
Rectangle {
id: iconContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.round(gridView.itemSize * 0.67)
color: Color.transparent
property bool isImage: {
if (model.fileIsDir)
return false
const ext = model.fileName.split('.').pop().toLowerCase()
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext)
}
Image {
id: thumbnail
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
source: iconContainer.isImage ? "file://" + model.filePath : ""
fillMode: Image.PreserveAspectFit
visible: iconContainer.isImage && status === Image.Ready
smooth: false
cache: true
asynchronous: true
sourceSize.width: 120 * scaling
sourceSize.height: 120 * scaling
onStatusChanged: {
if (status === Image.Error)
visible = false
}
Rectangle {
anchors.fill: parent
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
visible: thumbnail.status === Image.Loading
NIcon {
icon: "filepicker-photo"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
anchors.centerIn: parent
}
}
}
NIcon {
icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName)
font.pointSize: Style.fontSizeXXL * 2 * scaling
color: {
if (isSelected)
return Color.mSecondary
else if (mouseArea.containsMouse)
return model.fileIsDir ? Color.mOnTertiary : Color.mOnTertiary
else
return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant
}
anchors.centerIn: parent
visible: !iconContainer.isImage || thumbnail.status !== Image.Ready
}
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
width: 24 * scaling
height: 24 * scaling
radius: width / 2
color: Color.mSecondary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: isSelected
NIcon {
icon: "filepicker-check"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
}
NText {
text: model.fileName
color: {
if (isSelected)
return Color.mSecondary
else if (mouseArea.containsMouse)
return Color.mOnTertiary
else
return Color.mOnSurface
}
font.pointSize: Style.fontSizeS * scaling
font.weight: isSelected ? Style.fontWeightBold : Style.fontWeightRegular
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
elide: Text.ElideRight
maximumLineCount: 2
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (model.fileIsDir) {
if (root.selectFolders && !root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
} else {
folderModel.folder = "file://" + model.filePath
root.currentPath = model.filePath
}
} else {
if (root.selectFiles)
filePickerPanel.currentSelection = [model.filePath]
}
}
}
onDoubleClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (model.fileIsDir) {
if (root.selectFolders && !root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
root.confirmSelection()
} else {
folderModel.folder = "file://" + model.filePath
root.currentPath = model.filePath
}
} else {
if (root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
root.confirmSelection()
}
}
}
}
}
}
}
// List view
ListView {
id: listView
anchors.fill: parent
anchors.margins: Style.marginS * scaling
model: filteredModel
visible: !filePickerPanel.viewMode
clip: true
ScrollBar.vertical: scrollBarComponent.createObject(listView, {
"parent": listView,
"x": listView.mirrored ? 0 : listView.width - width,
"y": 0,
"height": listView.height
})
delegate: Rectangle {
id: listItem
width: listView.width
height: 40 * scaling
color: {
if (filePickerPanel.currentSelection.includes(model.filePath))
return Color.mSecondary
if (mouseArea.containsMouse)
return Qt.alpha(Color.mOnSurface, 0.1)
return Color.transparent
}
radius: Style.radiusS * scaling
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
NIcon {
icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName)
font.pointSize: Style.fontSizeL * scaling
color: model.fileIsDir ? (filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant
}
NText {
text: model.fileName
color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling
font.weight: filePickerPanel.currentSelection.includes(model.filePath) ? Style.fontWeightBold : Style.fontWeightRegular
Layout.fillWidth: true
elide: Text.ElideRight
}
NText {
text: model.fileIsDir ? "" : root.formatFileSize(model.fileSize)
color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
visible: !model.fileIsDir
Layout.preferredWidth: implicitWidth
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (model.fileIsDir) {
if (root.selectFolders && !root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
} else {
folderModel.folder = "file://" + model.filePath
root.currentPath = model.filePath
}
} else {
if (root.selectFiles)
filePickerPanel.currentSelection = [model.filePath]
}
}
}
onDoubleClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (model.fileIsDir) {
if (root.selectFolders && !root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
root.confirmSelection()
} else {
folderModel.folder = "file://" + model.filePath
root.currentPath = model.filePath
}
} else {
if (root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
root.confirmSelection()
}
}
}
}
}
}
}
}
// Footer
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: {
if (filePickerPanel.searchText.length > 0) {
return "Searching for: \"" + filePickerPanel.searchText + "\" (" + filteredModel.count + " matches)"
} else if (filePickerPanel.currentSelection.length > 0) {
return filePickerPanel.currentSelection.length + " item(s) selected"
} else {
return filteredModel.count + " items"
}
}
color: filePickerPanel.searchText.length > 0 ? Color.mPrimary : Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: {
root.cancelled()
root.close()
}
}
NButton {
text: {
if (root.selectFolders && !root.selectFiles)
return "Select Folder"
else if (root.selectFiles && !root.selectFolders)
return "Select File"
else
return "Select"
}
icon: "filepicker-check"
enabled: filePickerPanel.currentSelection.length > 0
onClicked: root.confirmSelection()
}
}
// Get default folder with proper fallback
function getDefaultFolder() {
if (root.initialPath) {
return "file:///" + root.initialPath.replace(/^\//, "")
}
Connections {
target: root
function onShouldResetSelectionChanged() {
if (root.shouldResetSelection) {
filePickerPanel.currentSelection = []
root.shouldResetSelection = false
// Fallback to home directory
try {
return StandardPaths.writableLocation(StandardPaths.HomeLocation)
} catch (e) {
// Final fallback if StandardPaths fails
return "file:///" + (Qt.platform.os === "windows" ? "C:/Users" : "/home")
}
}
// FileDialog for file selection (Qt 6.x)
FileDialog {
id: fileDialog
title: root.title
currentFolder: getDefaultFolder()
fileMode: root.multipleSelection ? FileDialog.OpenFiles : FileDialog.OpenFile
nameFilters: root.nameFilters
acceptLabel: root.acceptLabel
rejectLabel: root.rejectLabel
modality: Qt.WindowModal
onAccepted: {
if (fileMode === FileDialog.OpenFiles) {
var paths = []
for (var i = 0; i < fileDialog.selectedFiles.length; i++) {
paths.push(urlToPath(fileDialog.selectedFiles[i]))
}
root.selectedPaths = paths
root.selectedPath = paths.length > 0 ? paths[0] : ""
root.pathsSelected(paths)
root.accepted(paths)
} else {
var singlePath = urlToPath(fileDialog.selectedFile)
root.selectedPath = singlePath
root.selectedPaths = [singlePath]
root.pathSelected(singlePath)
root.accepted([singlePath])
}
root.handleClose()
}
Component.onCompleted: {
if (!root.currentPath)
root.currentPath = root.initialPath
folderModel.folder = "file://" + root.currentPath
onRejected: {
root.rejected()
root.handleClose()
}
}
// FolderDialog for folder selection (Qt 6.x)
FolderDialog {
id: folderDialog
title: root.title
currentFolder: getDefaultFolder()
acceptLabel: root.acceptLabel
rejectLabel: root.rejectLabel
modality: Qt.WindowModal
onAccepted: {
var folderPath = urlToPath(folderDialog.selectedFolder)
root.selectedPath = folderPath
root.selectedPaths = [folderPath]
root.pathSelected(folderPath)
root.accepted([folderPath])
root.handleClose()
}
onRejected: {
root.rejected()
root.handleClose()
}
}
}
+5
View File
@@ -41,6 +41,11 @@ Popup {
selectedIcon = initialIcon
query = initialIcon
searchInput.forceActiveFocus()
PanelService.willOpenPopup(root)
}
onClosed: {
PanelService.willClosePopup(root)
}
background: Rectangle {
-1
View File
@@ -47,7 +47,6 @@ RowLayout {
backgroundColor: Color.mSecondary
textColor: Color.mOnSecondary
hoverColor: Color.mTertiary
pressColor: Color.mPrimary
enabled: root.actionButtonEnabled
onClicked: {
+10 -2
View File
@@ -25,6 +25,8 @@ Loader {
property bool panelAnchorLeft: false
property bool panelAnchorRight: false
property bool isMasked: false
// Properties to support positioning relative to the opener (button)
property bool useButtonPosition: false
property point buttonPosition: Qt.point(0, 0)
@@ -177,12 +179,18 @@ Loader {
}
visible: true
color: Settings.data.general.dimDesktop ? Qt.alpha(Color.mShadow, dimmingOpacity) : Color.transparent
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "noctalia-panel"
WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
mask: root.isMasked ? maskRegion : null
Region {
id: maskRegion
}
Behavior on color {
ColorAnimation {
duration: Style.animationSlow
@@ -244,7 +252,7 @@ Loader {
}
scale: root.scaleValue
opacity: root.opacityValue
opacity: root.isMasked ? 0 : root.opacityValue
x: isDragged ? manualX : calculatedX
y: isDragged ? manualY : calculatedY
+11 -3
View File
@@ -153,6 +153,14 @@ RowLayout {
height: root.popupHeight + 60 * scaling
padding: Style.marginM * scaling
onOpened: {
PanelService.willOpenPopup(root)
}
onClosed: {
PanelService.willClosePopup(root)
}
contentItem: ColumnLayout {
spacing: Style.marginS * scaling
@@ -166,13 +174,13 @@ RowLayout {
fontSize: Style.fontSizeS * scaling
}
ListView {
NListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: combo.popup.visible ? filteredModel : null
ScrollIndicator.vertical: ScrollIndicator {}
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
delegate: ItemDelegate {
width: listView.width
+3
View File
@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
@@ -14,4 +15,6 @@ Text {
color: Color.mOnSurface
renderType: Text.QtRendering
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
+61 -28
View File
@@ -6,32 +6,41 @@
* but proper credit must be given to the original author.
*/
// Disable reload popup add this as a new row: //pragma Env QS_NO_RELOAD_POPUP=1
// Qt & Quickshell Core
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Widgets
// Commons & Services
import qs.Commons
import qs.Modules.Launcher
import qs.Modules.Background
import qs.Modules.Bar
import qs.Modules.Bar.Extras
import qs.Modules.BluetoothPanel
import qs.Modules.Calendar
import qs.Modules.Dock
import qs.Modules.IPC
import qs.Modules.LockScreen
import qs.Modules.Notification
import qs.Modules.SettingsPanel
import qs.Modules.PowerPanel
import qs.Modules.SidePanel
import qs.Modules.Toast
import qs.Modules.WiFiPanel
import qs.Modules.WallpaperSelector
import qs.Services
import qs.Widgets
// Core Modules
import qs.Modules.Background
import qs.Modules.Dock
import qs.Modules.LockScreen
import qs.Modules.SessionMenu
// Bar & Bar Components
import qs.Modules.Bar
import qs.Modules.Bar.Extras
import qs.Modules.Bar.Bluetooth
import qs.Modules.Bar.Calendar
import qs.Modules.Bar.WiFi
// Panels & UI Components
import qs.Modules.ControlCenter
import qs.Modules.Launcher
import qs.Modules.Notification
import qs.Modules.OSD
import qs.Modules.Settings
import qs.Modules.Toast
import qs.Modules.Wallpaper
ShellRoot {
id: shellRoot
@@ -51,7 +60,24 @@ ShellRoot {
ToastOverlay {}
IPCManager {}
// OSD overlays for volume and brightness
OSD {
id: volumeOSD
objectName: "volumeOSD"
osdType: OSD.Type.Volume
onOsdShowing: brightnessOSD.hideOSD()
}
OSD {
id: brightnessOSD
objectName: "brightnessOSD"
osdType: OSD.Type.Brightness
onOsdShowing: volumeOSD.hideOSD()
}
// IPCService is treated as a service
// but it's actually an Item that needs to exists in the shell.
IPCService {}
// ------------------------------
// All the NPanels
@@ -60,12 +86,12 @@ ShellRoot {
objectName: "launcherPanel"
}
SidePanel {
id: sidePanel
objectName: "sidePanel"
ControlCenterPanel {
id: controlCenterPanel
objectName: "controlCenterPanel"
}
Calendar {
CalendarPanel {
id: calendarPanel
objectName: "calendarPanel"
}
@@ -80,9 +106,9 @@ ShellRoot {
objectName: "notificationHistoryPanel"
}
PowerPanel {
id: powerPanel
objectName: "powerPanel"
SessionMenu {
id: sessionMenuPanel
objectName: "sessionMenuPanel"
}
WiFiPanel {
@@ -95,13 +121,20 @@ ShellRoot {
objectName: "bluetoothPanel"
}
WallpaperSelector {
id: wallpaperSelector
objectName: "wallpaperSelector"
WallpaperPanel {
id: wallpaperPanel
objectName: "wallpaperPanel"
}
Component.onCompleted: {
// Save a ref. to our lockScreen so we can access it easily
PanelService.lockScreen = lockScreen
}
Connections {
target: Quickshell
function onReloadCompleted() {
Quickshell.inhibitReloadPopup()
}
}
}