mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge remote-tracking branch 'upstream/main' into notifications-refine
Resolve conflicts due to project structure changes
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -4,7 +4,6 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Commons.IconsSets
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
+22
-10
@@ -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"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Widgets
|
||||
|
||||
Variants {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
+6
-6
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+49
@@ -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
|
||||
+56
-42
@@ -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]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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 {
|
||||
+15
-14
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -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."
|
||||
+12
-12
@@ -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]
|
||||
}
|
||||
}
|
||||
+20
-34
@@ -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])
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -18,7 +18,8 @@ NPanel {
|
||||
panelAnchorHorizontalCenter: true
|
||||
panelAnchorVerticalCenter: true
|
||||
panelKeyboardFocus: true
|
||||
draggable: true
|
||||
|
||||
draggable: !PanelService.hasOpenedPopup
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: wallpaperPanel
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ Popup {
|
||||
selectedIcon = initialIcon
|
||||
query = initialIcon
|
||||
searchInput.forceActiveFocus()
|
||||
PanelService.willOpenPopup(root)
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
PanelService.willClosePopup(root)
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
|
||||
@@ -47,7 +47,6 @@ RowLayout {
|
||||
backgroundColor: Color.mSecondary
|
||||
textColor: Color.mOnSecondary
|
||||
hoverColor: Color.mTertiary
|
||||
pressColor: Color.mPrimary
|
||||
enabled: root.actionButtonEnabled
|
||||
|
||||
onClicked: {
|
||||
|
||||
+10
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user