1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

Modularlize the shell

This commit is contained in:
bbedward
2025-07-10 16:40:04 -04:00
parent 7cdeba1625
commit 40b2a3af1e
28 changed files with 5260 additions and 4906 deletions

944
Widgets/AppLauncher.qml Normal file
View File

@@ -0,0 +1,944 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import "../Common"
PanelWindow {
id: launcher
property var theme
property bool isVisible: false
// Default theme fallback
property var defaultTheme: QtObject {
property color primary: "#D0BCFF"
property color background: "#10121E"
property color surfaceContainer: "#1D1B20"
property color surfaceText: "#E6E0E9"
property color surfaceVariant: "#49454F"
property color surfaceVariantText: "#CAC4D0"
property color outline: "#938F99"
property real cornerRadius: 12
property real cornerRadiusLarge: 16
property real cornerRadiusXLarge: 24
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeLarge: 16
property real fontSizeMedium: 14
property real fontSizeSmall: 12
property real iconSize: 24
property real iconSizeLarge: 32
property real barHeight: 48
property string iconFont: "Material Symbols Rounded"
property int iconFontWeight: Font.Normal
property int shortDuration: 150
property int mediumDuration: 300
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
}
property var activeTheme: theme || defaultTheme
// Full screen overlay setup for proper focus
anchors {
top: true
left: true
right: true
bottom: true
}
// Proper layer shell configuration
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-launcher"
visible: isVisible
color: "transparent"
// Enhanced app management
property var currentApp: ({})
property var allApps: []
property var categories: ["All"]
property string selectedCategory: "All"
property var recentApps: []
property var pinnedApps: ["firefox", "code", "terminal", "file-manager"]
property bool showCategories: false
property string viewMode: "list" // "list" or "grid"
property var appCategories: ({
"AudioVideo": "Media",
"Audio": "Media",
"Video": "Media",
"Development": "Development",
"TextEditor": "Development",
"IDE": "Development",
"Programming": "Development",
"Education": "Education",
"Game": "Games",
"Graphics": "Graphics",
"Photography": "Graphics",
"Network": "Internet",
"WebBrowser": "Internet",
"Office": "Office",
"WordProcessor": "Office",
"Spreadsheet": "Office",
"Presentation": "Office",
"Science": "Science",
"Settings": "Settings",
"System": "System",
"Utility": "Utilities",
"Accessories": "Utilities",
"FileManager": "Utilities",
"TerminalEmulator": "Utilities"
})
ListModel { id: filteredModel }
ListModel { id: categoryModel }
// Background dim with click to close
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.3)
opacity: launcher.isVisible ? 1.0 : 0.0
visible: launcher.isVisible
Behavior on opacity {
NumberAnimation {
duration: activeTheme.shortDuration
easing.type: activeTheme.standardEasing
}
}
MouseArea {
anchors.fill: parent
enabled: launcher.isVisible
onClicked: launcher.hide()
}
}
// Desktop applications scanning
Process {
id: desktopScanner
command: ["sh", "-c", `
for dir in "/usr/share/applications/" "/usr/local/share/applications/" "$HOME/.local/share/applications/" "/run/current-system/sw/share/applications/"; do
if [ -d "$dir" ]; then
find "$dir" -name "*.desktop" 2>/dev/null | while read file; do
echo "===FILE:$file"
sed -n '/^\\[Desktop Entry\\]/,/^\\[.*\\]/{/^\\[Desktop Entry\\]/d; /^\\[.*\\]/q; /^Name=/p; /^Exec=/p; /^Icon=/p; /^Hidden=/p; /^NoDisplay=/p; /^Categories=/p; /^Comment=/p}' "$file" 2>/dev/null || true
done
fi
done
`]
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.startsWith("===FILE:")) {
// Save previous app if valid
if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) {
allApps.push({
name: currentApp.name,
exec: currentApp.exec,
icon: currentApp.icon || "application-x-executable",
comment: currentApp.comment || "",
categories: currentApp.categories || []
})
}
// Start new app
currentApp = { name: "", exec: "", icon: "", comment: "", categories: [], hidden: false, noDisplay: false }
} else if (line.startsWith("Name=")) {
currentApp.name = line.substring(5)
} else if (line.startsWith("Exec=")) {
currentApp.exec = line.substring(5)
} else if (line.startsWith("Icon=")) {
currentApp.icon = line.substring(5)
} else if (line.startsWith("Comment=")) {
currentApp.comment = line.substring(8)
} else if (line.startsWith("Categories=")) {
currentApp.categories = line.substring(11).split(";").filter(cat => cat.length > 0)
} else if (line === "Hidden=true") {
currentApp.hidden = true
} else if (line === "NoDisplay=true") {
currentApp.noDisplay = true
}
}
}
onExited: {
// Save last app
if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) {
allApps.push({
name: currentApp.name,
exec: currentApp.exec,
icon: currentApp.icon || "application-x-executable",
comment: currentApp.comment || "",
categories: currentApp.categories || []
})
}
// Extract unique categories
let uniqueCategories = new Set(["All"])
allApps.forEach(app => {
app.categories.forEach(cat => {
if (appCategories[cat]) {
uniqueCategories.add(appCategories[cat])
}
})
})
categories = Array.from(uniqueCategories)
console.log("Loaded", allApps.length, "applications with", categories.length, "categories")
updateFilteredModel()
}
}
function updateFilteredModel() {
filteredModel.clear()
let apps = allApps
// Filter by category
if (selectedCategory !== "All") {
apps = apps.filter(app => {
return app.categories.some(cat => appCategories[cat] === selectedCategory)
})
}
// Filter by search
if (searchField.text.length > 0) {
const query = searchField.text.toLowerCase()
apps = apps.filter(app => {
return app.name.toLowerCase().includes(query) ||
(app.comment && app.comment.toLowerCase().includes(query))
}).sort((a, b) => {
// Sort by relevance
const aName = a.name.toLowerCase()
const bName = b.name.toLowerCase()
const aStartsWith = aName.startsWith(query)
const bStartsWith = bName.startsWith(query)
if (aStartsWith && !bStartsWith) return -1
if (!aStartsWith && bStartsWith) return 1
return aName.localeCompare(bName)
})
}
// Sort alphabetically if no search
if (searchField.text.length === 0) {
apps.sort((a, b) => a.name.localeCompare(b.name))
}
// Add to model
apps.forEach(app => {
filteredModel.append(app)
})
}
// Main launcher panel with enhanced design
Rectangle {
id: launcherPanel
width: 520
height: 600
anchors {
top: parent.top
left: parent.left
topMargin: 50
leftMargin: activeTheme.spacingL
}
color: Qt.rgba(activeTheme.surfaceContainer.r, activeTheme.surfaceContainer.g, activeTheme.surfaceContainer.b, 0.98)
radius: activeTheme.cornerRadiusXLarge
// Material 3 elevation with multiple layers
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.08)
border.width: 1
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12)
border.width: 1
radius: parent.radius
z: -1
}
// Animated entrance with spring effect
transform: [
Scale {
id: scaleTransform
origin.x: 0
origin.y: 0
xScale: launcher.isVisible ? 1.0 : 0.92
yScale: launcher.isVisible ? 1.0 : 0.92
Behavior on xScale {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
Behavior on yScale {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
},
Translate {
id: translateTransform
x: launcher.isVisible ? 0 : -30
y: launcher.isVisible ? 0 : -15
Behavior on x {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
Behavior on y {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
}
]
opacity: launcher.isVisible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
// Content with focus management
Item {
anchors.fill: parent
focus: true
// Handle keyboard shortcuts
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
launcher.hide()
event.accepted = true
}
}
Column {
anchors.fill: parent
anchors.margins: activeTheme.spacingXL
spacing: activeTheme.spacingL
// Header section
Row {
width: parent.width
height: 40
// App launcher title
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Applications"
font.pixelSize: activeTheme.fontSizeLarge + 4
font.weight: Font.Bold
color: activeTheme.surfaceText
}
Item { width: parent.width - 200; height: 1 }
// Quick stats
Text {
anchors.verticalCenter: parent.verticalCenter
text: filteredModel.count + " apps"
font.pixelSize: activeTheme.fontSizeMedium
color: activeTheme.surfaceVariantText
}
}
// Enhanced search field
Rectangle {
id: searchContainer
width: parent.width
height: 52
radius: activeTheme.cornerRadiusLarge
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.6)
border.width: searchField.activeFocus ? 2 : 1
border.color: searchField.activeFocus ? activeTheme.primary :
Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.3)
Behavior on border.color {
ColorAnimation {
duration: activeTheme.shortDuration
easing.type: activeTheme.standardEasing
}
}
Row {
anchors.fill: parent
anchors.leftMargin: activeTheme.spacingL
anchors.rightMargin: activeTheme.spacingL
spacing: activeTheme.spacingM
Text {
anchors.verticalCenter: parent.verticalCenter
text: "search"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: searchField.activeFocus ? activeTheme.primary : activeTheme.surfaceVariantText
font.weight: activeTheme.iconFontWeight
}
TextInput {
id: searchField
anchors.verticalCenter: parent.verticalCenter
width: parent.width - parent.spacing - activeTheme.iconSize - 32
height: parent.height - activeTheme.spacingS
color: activeTheme.surfaceText
font.pixelSize: activeTheme.fontSizeLarge
verticalAlignment: TextInput.AlignVCenter
focus: launcher.isVisible
selectByMouse: true
activeFocusOnTab: true
// Placeholder text
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Search applications..."
color: activeTheme.surfaceVariantText
font.pixelSize: activeTheme.fontSizeLarge
visible: searchField.text.length === 0 && !searchField.activeFocus
}
// Clear button
Rectangle {
width: 24
height: 24
radius: 12
color: clearSearchArea.containsMouse ? Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12) : "transparent"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: searchField.text.length > 0
Text {
anchors.centerIn: parent
text: "close"
font.family: activeTheme.iconFont
font.pixelSize: 16
color: clearSearchArea.containsMouse ? activeTheme.outline : activeTheme.surfaceVariantText
}
MouseArea {
id: clearSearchArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: searchField.text = ""
}
}
onTextChanged: updateFilteredModel()
Keys.onPressed: function (event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count) {
launcher.launchApp(filteredModel.get(0).exec)
launcher.hide()
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
launcher.hide()
event.accepted = true
}
}
}
}
}
// Category filter and view mode controls
Row {
width: parent.width
height: 40
spacing: activeTheme.spacingM
visible: searchField.text.length === 0
// Category filter
Rectangle {
width: 200
height: 36
radius: activeTheme.cornerRadius
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: activeTheme.spacingS
Text {
text: "category"
font.family: activeTheme.iconFont
font.pixelSize: 18
color: activeTheme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: selectedCategory
font.pixelSize: activeTheme.fontSizeMedium
color: activeTheme.surfaceText
anchors.verticalCenter: parent.verticalCenter
font.weight: Font.Medium
}
}
Text {
anchors.right: parent.right
anchors.rightMargin: activeTheme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: showCategories ? "expand_less" : "expand_more"
font.family: activeTheme.iconFont
font.pixelSize: 18
color: activeTheme.surfaceVariantText
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: showCategories = !showCategories
}
}
Item { width: parent.width - 300; height: 1 }
// View mode toggle
Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
// List view button
Rectangle {
width: 36
height: 36
radius: activeTheme.cornerRadius
color: viewMode === "list" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) :
listViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
text: "view_list"
font.family: activeTheme.iconFont
font.pixelSize: 20
color: viewMode === "list" ? activeTheme.primary : activeTheme.surfaceText
}
MouseArea {
id: listViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: viewMode = "list"
}
}
// Grid view button
Rectangle {
width: 36
height: 36
radius: activeTheme.cornerRadius
color: viewMode === "grid" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) :
gridViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
text: "grid_view"
font.family: activeTheme.iconFont
font.pixelSize: 20
color: viewMode === "grid" ? activeTheme.primary : activeTheme.surfaceText
}
MouseArea {
id: gridViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: viewMode = "grid"
}
}
}
}
// Category dropdown
Rectangle {
width: 200
height: Math.min(250, categories.length * 40 + activeTheme.spacingM * 2)
radius: activeTheme.cornerRadiusLarge
color: activeTheme.surfaceContainer
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: 1
visible: showCategories
z: 100
// Drop shadow
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.1)
border.width: 1
z: -1
}
ScrollView {
anchors.fill: parent
anchors.margins: activeTheme.spacingS
clip: true
ListView {
model: categories
spacing: 4
delegate: Rectangle {
width: ListView.view.width
height: 36
radius: activeTheme.cornerRadiusSmall
color: catArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) : "transparent"
Text {
anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: activeTheme.fontSizeMedium
color: selectedCategory === modelData ? activeTheme.primary : activeTheme.surfaceText
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
}
MouseArea {
id: catArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
showCategories = false
updateFilteredModel()
}
}
}
}
}
}
// App grid/list container
Rectangle {
width: parent.width
height: parent.height - searchContainer.height - (searchField.text.length === 0 ? 128 : 60) - parent.spacing * 3
color: "transparent"
ScrollView {
anchors.fill: parent
clip: true
Item {
anchors.fill: parent
// List view
ListView {
id: appList
anchors.fill: parent
anchors.margins: activeTheme.spacingS
spacing: activeTheme.spacingS
visible: viewMode === "list"
model: filteredModel
delegate: Rectangle {
width: appList.width
height: 72
radius: activeTheme.cornerRadiusLarge
color: appMouseArea.hovered ?
Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08)
border.width: 1
Behavior on color {
ColorAnimation {
duration: activeTheme.shortDuration
easing.type: activeTheme.standardEasing
}
}
Row {
anchors.fill: parent
anchors.margins: activeTheme.spacingM
spacing: activeTheme.spacingL
Item {
width: 56
height: 56
anchors.verticalCenter: parent.verticalCenter
IconImage {
anchors.fill: parent
source: Quickshell.iconPath(model.icon, "application-x-executable")
smooth: true
visible: status === Image.Ready
onStatusChanged: {
if (status === Image.Error && model.name.includes("Avahi")) {
console.log("Avahi icon failed to load:", model.icon, "->", source)
}
}
}
// Fallback for missing icons
Rectangle {
anchors.fill: parent
visible: !parent.children[0].visible
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
radius: activeTheme.cornerRadiusLarge
Text {
anchors.centerIn: parent
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: activeTheme.iconSizeLarge
color: activeTheme.surfaceVariantText
font.weight: Font.Medium
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 56 - activeTheme.spacingL
spacing: activeTheme.spacingXS
Text {
width: parent.width
text: model.name
font.pixelSize: activeTheme.fontSizeLarge
color: activeTheme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
Text {
width: parent.width
text: model.comment || "Application"
font.pixelSize: activeTheme.fontSizeMedium
color: activeTheme.surfaceVariantText
elide: Text.ElideRight
visible: model.comment && model.comment.length > 0
}
}
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
launcher.launchApp(model.exec)
launcher.hide()
}
}
}
}
// Grid view
GridView {
id: appGrid
anchors.fill: parent
anchors.margins: activeTheme.spacingS
// Responsive cell sizes based on screen width
property int baseCellWidth: Math.max(100, Math.min(140, width / 8))
property int baseCellHeight: baseCellWidth + 20
cellWidth: baseCellWidth
cellHeight: baseCellHeight
visible: viewMode === "grid"
// Center the grid content
property int columnsCount: Math.floor(width / cellWidth)
property int remainingSpace: width - (columnsCount * cellWidth)
anchors.leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2)
anchors.rightMargin: anchors.leftMargin
model: filteredModel
delegate: Rectangle {
width: appGrid.cellWidth - 8
height: appGrid.cellHeight - 8
radius: activeTheme.cornerRadiusLarge
color: gridAppArea.hovered ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08)
border.width: 1
Behavior on color {
ColorAnimation {
duration: activeTheme.shortDuration
easing.type: activeTheme.standardEasing
}
}
Column {
anchors.centerIn: parent
spacing: activeTheme.spacingS
Item {
property int iconSize: Math.min(56, Math.max(32, appGrid.cellWidth * 0.6))
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
anchors.fill: parent
source: Quickshell.iconPath(model.icon, "application-x-executable")
smooth: true
visible: status === Image.Ready
onStatusChanged: {
if (status === Image.Error && model.name.includes("Avahi")) {
console.log("Avahi grid icon failed to load:", model.icon, "->", source)
}
}
}
// Fallback for missing icons
Rectangle {
anchors.fill: parent
visible: !parent.children[0].visible
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
radius: activeTheme.cornerRadiusLarge
Text {
anchors.centerIn: parent
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: activeTheme.iconSizeLarge
color: activeTheme.surfaceVariantText
font.weight: Font.Medium
}
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
width: 88
text: model.name
font.pixelSize: activeTheme.fontSizeSmall
color: activeTheme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: gridAppArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
launcher.launchApp(model.exec)
launcher.hide()
}
}
}
}
}
}
}
}
}
}
Process {
id: appLauncher
function start(exec) {
// Clean up exec command (remove field codes)
var cleanExec = exec.replace(/%[fFuU]/g, "").trim()
command = ["sh", "-c", cleanExec]
running = true
}
onExited: {
if (exitCode !== 0) {
console.log("Failed to launch application, exit code:", exitCode)
}
}
}
function launchApp(exec) {
appLauncher.start(exec)
}
function show() {
launcher.isVisible = true
Qt.callLater(function() {
searchField.forceActiveFocus()
})
}
function hide() {
launcher.isVisible = false
searchField.text = ""
showCategories = false
}
function toggle() {
if (launcher.isVisible) {
hide()
} else {
show()
}
}
Component.onCompleted: {
desktopScanner.running = true
}
}

562
Widgets/CalendarPopup.qml Normal file
View File

@@ -0,0 +1,562 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Services.Mpris
import "../Common"
PanelWindow {
id: calendarPopup
visible: root.calendarVisible
implicitWidth: 320
implicitHeight: 400
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
property date displayDate: new Date()
property date selectedDate: new Date()
Rectangle {
width: 400
height: root.hasActiveMedia ? 580 : (root.weather.available ? 480 : 400)
x: (parent.width - width) / 2
y: Theme.barHeight + Theme.spacingS
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: root.calendarVisible ? 1.0 : 0.0
scale: root.calendarVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
// Media Player (when active)
Rectangle {
visible: root.hasActiveMedia
width: parent.width
height: 180
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
height: 100
spacing: Theme.spacingM
Rectangle {
width: 100
height: 100
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
anchors.fill: parent
source: root.activePlayer?.trackArtUrl || ""
fillMode: Image.PreserveAspectCrop
smooth: true
}
Rectangle {
anchors.fill: parent
visible: parent.children[0].status !== Image.Ready
color: "transparent"
Text {
anchors.centerIn: parent
text: "album"
font.family: Theme.iconFont
font.pixelSize: 48
color: Theme.surfaceVariantText
}
}
}
}
Column {
width: parent.width - 100 - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
Text {
text: root.activePlayer?.trackTitle || "Unknown Track"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: root.activePlayer?.trackArtist || "Unknown Artist"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
}
Text {
text: root.activePlayer?.trackAlbum || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Progress bar
Rectangle {
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Rectangle {
width: parent.width * (root.activePlayer?.position / Math.max(root.activePlayer?.length || 1, 1))
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (root.activePlayer && root.activePlayer.length > 0) {
const ratio = mouse.x / width
const newPosition = ratio * root.activePlayer.length
console.log("Seeking to position:", newPosition, "ratio:", ratio, "canSeek:", root.activePlayer.canSeek)
if (root.activePlayer.canSeek) {
root.activePlayer.position = newPosition
} else {
console.log("Player does not support seeking")
}
}
}
}
}
// Control buttons
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingL
Rectangle {
width: 36
height: 36
radius: 18
color: prevBtnAreaCal.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.surfaceText
}
MouseArea {
id: prevBtnAreaCal
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activePlayer?.previous()
}
}
Rectangle {
width: 40
height: 40
radius: 20
color: Theme.primary
Text {
anchors.centerIn: parent
text: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
font.family: Theme.iconFont
font.pixelSize: 24
color: Theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activePlayer?.togglePlaying()
}
}
Rectangle {
width: 36
height: 36
radius: 18
color: nextBtnAreaCal.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.surfaceText
}
MouseArea {
id: nextBtnAreaCal
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activePlayer?.next()
}
}
}
}
}
// Weather header (when available and no media)
Rectangle {
visible: root.weather.available && !root.hasActiveMedia
width: parent.width
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
// Weather icon and temp
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: root.weatherIcons[root.weather.wCode] || "clear_day"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 4
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.useFahrenheit = !root.useFahrenheit
}
}
Text {
text: root.weather.city
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Weather details grid
Grid {
columns: 2
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingXS
Text {
text: "humidity_low"
font.family: Theme.iconFont
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.weather.humidity + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
Text {
text: "air"
font.family: Theme.iconFont
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.weather.wind
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
Text {
text: "wb_twilight"
font.family: Theme.iconFont
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.weather.sunrise
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
Text {
text: "bedtime"
font.family: Theme.iconFont
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.weather.sunset
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Row {
width: parent.width
height: 40
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "chevron_left"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.primary
font.weight: Theme.iconFontWeight
}
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(calendarPopup.displayDate)
newDate.setMonth(newDate.getMonth() - 1)
calendarPopup.displayDate = newDate
}
}
}
Text {
width: parent.width - 80
height: 40
text: Qt.formatDate(calendarPopup.displayDate, "MMMM yyyy")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "chevron_right"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.primary
font.weight: Theme.iconFontWeight
}
MouseArea {
id: nextMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(calendarPopup.displayDate)
newDate.setMonth(newDate.getMonth() + 1)
calendarPopup.displayDate = newDate
}
}
}
}
Row {
width: parent.width
height: 32
Repeater {
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Rectangle {
width: parent.width / 7
height: 32
color: "transparent"
Text {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
Grid {
width: parent.width
height: root.hasActiveMedia ? parent.height - 300 : (root.weather.available ? parent.height - 200 : parent.height - 120)
columns: 7
rows: 6
property date firstDay: {
let date = new Date(calendarPopup.displayDate.getFullYear(), calendarPopup.displayDate.getMonth(), 1)
let dayOfWeek = date.getDay()
date.setDate(date.getDate() - dayOfWeek)
return date
}
Repeater {
model: 42
Rectangle {
width: parent.width / 7
height: parent.height / 6
property date dayDate: {
let date = new Date(parent.firstDay)
date.setDate(date.getDate() + index)
return date
}
property bool isCurrentMonth: dayDate.getMonth() === calendarPopup.displayDate.getMonth()
property bool isToday: dayDate.toDateString() === new Date().toDateString()
property bool isSelected: dayDate.toDateString() === calendarPopup.selectedDate.toDateString()
color: isSelected ? Theme.primary :
isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadiusSmall
Text {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeMedium
color: isSelected ? Theme.surface :
isToday ? Theme.primary :
isCurrentMonth ? Theme.surfaceText :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
calendarPopup.selectedDate = dayDate
}
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.calendarVisible = false
}
}
}

View File

@@ -0,0 +1,626 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import "../Common"
PanelWindow {
id: clipboardHistory
property var theme
property bool isVisible: false
// Default theme fallback
property var defaultTheme: QtObject {
property color primary: "#D0BCFF"
property color background: "#10121E"
property color surfaceContainer: "#1D1B20"
property color surfaceText: "#E6E0E9"
property color surfaceVariant: "#49454F"
property color surfaceVariantText: "#CAC4D0"
property color outline: "#938F99"
property color error: "#F2B8B5"
property real cornerRadius: 12
property real cornerRadiusLarge: 16
property real cornerRadiusXLarge: 24
property real cornerRadiusSmall: 8
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeLarge: 16
property real fontSizeMedium: 14
property real fontSizeSmall: 12
property real iconSize: 24
property real iconSizeLarge: 32
property string iconFont: "Material Symbols Rounded"
property int iconFontWeight: Font.Normal
property int shortDuration: 150
property int mediumDuration: 300
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
}
property var activeTheme: theme || defaultTheme
// Window properties
color: "transparent"
visible: isVisible
anchors {
top: true
left: true
right: true
bottom: true
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
// Clipboard entries model
property var clipboardEntries: []
ListModel {
id: clipboardModel
}
ListModel {
id: filteredClipboardModel
}
function updateFilteredModel() {
filteredClipboardModel.clear()
for (let i = 0; i < clipboardModel.count; i++) {
const entry = clipboardModel.get(i).entry
if (searchField.text.trim().length === 0) {
filteredClipboardModel.append({"entry": entry})
} else {
const content = getEntryPreview(entry).toLowerCase()
if (content.includes(searchField.text.toLowerCase())) {
filteredClipboardModel.append({"entry": entry})
}
}
}
}
function toggle() {
if (isVisible) {
hide()
} else {
show()
}
}
function show() {
clipboardHistory.isVisible = true
searchField.focus = true
refreshClipboard()
console.log("ClipboardHistory: Opening and refreshing")
}
function hide() {
clipboardHistory.isVisible = false
searchField.focus = false
searchField.text = ""
}
function refreshClipboard() {
clipboardProcess.running = true
}
function copyEntry(entry) {
const entryId = entry.split('\t')[0]
copyProcess.command = ["sh", "-c", `cliphist decode ${entryId} | wl-copy`]
copyProcess.running = true
hide()
}
function deleteEntry(entry) {
const entryId = entry.split('\t')[0]
deleteProcess.command = ["cliphist", "delete-query", entryId]
deleteProcess.running = true
}
function clearAll() {
clearProcess.running = true
}
function getEntryPreview(entry) {
// Remove cliphist ID prefix and clean up content
let content = entry.replace(/^\s*\d+\s+/, "")
// Handle different content types
if (content.includes("image/")) {
const match = content.match(/(\d+)x(\d+)/)
return match ? `Image ${match[1]}×${match[2]}` : "Image"
}
// Truncate long text
if (content.length > 100) {
return content.substring(0, 100) + "..."
}
return content
}
function getEntryType(entry) {
if (entry.includes("image/")) return "image"
if (entry.length > 200) return "long_text"
return "text"
}
// Background overlay
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: clipboardHistory.isVisible ? 1.0 : 0.0
visible: clipboardHistory.isVisible
Behavior on opacity {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
MouseArea {
anchors.fill: parent
enabled: clipboardHistory.isVisible
onClicked: clipboardHistory.hide()
}
}
// Main clipboard container
Rectangle {
id: clipboardContainer
width: Math.min(600, parent.width - 200)
height: Math.min(500, parent.height - 100)
anchors.centerIn: parent
color: activeTheme.surfaceContainer
radius: activeTheme.cornerRadiusXLarge
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: 1
opacity: clipboardHistory.isVisible ? 1.0 : 0.0
scale: clipboardHistory.isVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: activeTheme.mediumDuration
easing.type: activeTheme.emphasizedEasing
}
}
// Header section
Column {
id: headerSection
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: activeTheme.spacingXL
spacing: activeTheme.spacingL
// Title and actions
Row {
width: parent.width
height: 40
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Clipboard History"
font.pixelSize: activeTheme.fontSizeLarge + 4
font.weight: Font.Bold
color: activeTheme.surfaceText
}
Item {
width: parent.width - 180 - (clearAllButton.visible ? 48 : 0)
height: 1
}
// Clear all button
Rectangle {
id: clearAllButton
width: 40
height: 32
radius: activeTheme.cornerRadius
color: clearArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: clipboardModel.count > 0
Text {
anchors.centerIn: parent
text: "delete_sweep"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: clearArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: clearAll()
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
// Close button
Rectangle {
width: 40
height: 32
radius: activeTheme.cornerRadius
color: closeArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 4
Text {
anchors.centerIn: parent
text: "close"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: closeArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: clipboardHistory.hide()
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
}
// Search field
Rectangle {
width: parent.width
height: 48
radius: activeTheme.cornerRadiusLarge
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
border.color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: searchField.focus ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: activeTheme.spacingM
Text {
text: "search"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize
color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
TextInput {
id: searchField
width: parent.parent.width - 80
height: parent.parent.height
font.pixelSize: activeTheme.fontSizeLarge
color: activeTheme.surfaceText
verticalAlignment: TextInput.AlignVCenter
onTextChanged: updateFilteredModel()
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
clipboardHistory.hide()
}
}
// Placeholder text
Text {
text: "Search clipboard entries..."
font: searchField.font
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
visible: searchField.text.length === 0 && !searchField.focus
}
}
}
Behavior on border.color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
}
// Clipboard entries
Rectangle {
anchors.top: headerSection.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: activeTheme.spacingXL
anchors.topMargin: activeTheme.spacingL
color: "transparent"
ScrollView {
anchors.fill: parent
clip: true
ListView {
id: clipboardList
model: filteredClipboardModel
spacing: activeTheme.spacingS
delegate: Rectangle {
width: clipboardList.width
height: Math.max(60, contentColumn.implicitHeight + activeTheme.spacingM * 2)
radius: activeTheme.cornerRadius
color: entryArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.05)
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.1)
border.width: 1
property string entryType: getEntryType(model.entry)
property string entryPreview: getEntryPreview(model.entry)
Row {
anchors.fill: parent
anchors.margins: activeTheme.spacingM
spacing: activeTheme.spacingL
// Entry type icon
Rectangle {
width: 36
height: 36
radius: activeTheme.cornerRadius
color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: {
switch (entryType) {
case "image": return "image"
case "long_text": return "subject"
default: return "content_paste"
}
}
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize - 4
color: activeTheme.primary
}
}
// Entry content
Column {
id: contentColumn
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 100
spacing: activeTheme.spacingXS
Text {
text: {
switch (entryType) {
case "image": return "Image • " + entryPreview
case "long_text": return "Long Text"
default: return "Text"
}
}
font.pixelSize: activeTheme.fontSizeSmall
color: activeTheme.primary
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
Text {
text: entryPreview
font.pixelSize: activeTheme.fontSizeMedium
color: activeTheme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 2
elide: Text.ElideRight
visible: entryType !== "image"
}
}
// Actions
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: activeTheme.spacingXS
// Copy button
Rectangle {
width: 28
height: 28
radius: activeTheme.cornerRadiusSmall
color: copyArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "content_copy"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize - 8
color: copyArea.containsMouse ? activeTheme.primary : activeTheme.surfaceText
}
MouseArea {
id: copyArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyEntry(model.entry)
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
// Delete button
Rectangle {
width: 28
height: 28
radius: activeTheme.cornerRadiusSmall
color: deleteArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "delete"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSize - 8
color: deleteArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
}
MouseArea {
id: deleteArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: deleteEntry(model.entry)
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
}
}
MouseArea {
id: entryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyEntry(model.entry)
}
Behavior on color {
ColorAnimation { duration: activeTheme.shortDuration }
}
}
}
// Empty state
Column {
anchors.centerIn: parent
spacing: activeTheme.spacingL
visible: filteredClipboardModel.count === 0
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "content_paste_off"
font.family: activeTheme.iconFont
font.pixelSize: activeTheme.iconSizeLarge + 16
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.3)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "No clipboard history"
font.pixelSize: activeTheme.fontSizeLarge
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Copy something to see it here"
font.pixelSize: activeTheme.fontSizeMedium
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.4)
}
}
}
}
}
// Clipboard processes
Process {
id: clipboardProcess
command: ["cliphist", "list"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.trim()) {
clipboardHistory.clipboardEntries.push(line)
clipboardModel.append({"entry": line})
}
}
}
onStarted: {
clipboardHistory.clipboardEntries = []
clipboardModel.clear()
console.log("ClipboardHistory: Starting cliphist process...")
}
onExited: (exitCode) => {
if (exitCode === 0) {
updateFilteredModel()
} else {
console.warn("ClipboardHistory: Failed to load clipboard history")
}
}
}
Process {
id: copyProcess
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("ClipboardHistory: Failed to copy entry")
}
}
}
Process {
id: deleteProcess
running: false
onExited: (exitCode) => {
if (exitCode === 0) {
refreshClipboard()
}
}
}
Process {
id: clearProcess
command: ["cliphist", "wipe"]
running: false
onExited: (exitCode) => {
if (exitCode === 0) {
clipboardHistory.clipboardEntries = []
clipboardModel.clear()
}
}
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
hide()
}
}
}

File diff suppressed because it is too large Load Diff

150
Widgets/CustomSlider.qml Normal file
View File

@@ -0,0 +1,150 @@
import QtQuick
import "../Common"
Item {
id: slider
property int value: 50
property int minimum: 0
property int maximum: 100
property string leftIcon: ""
property string rightIcon: ""
property bool enabled: true
property string unit: "%"
property bool showValue: true
signal sliderValueChanged(int newValue)
height: 80
Column {
anchors.fill: parent
spacing: Theme.spacingM
// Value display
Text {
text: slider.value + slider.unit
font.pixelSize: Theme.fontSizeMedium
color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText
font.weight: Font.Medium
visible: slider.showValue
anchors.horizontalCenter: parent.horizontalCenter
}
// Slider row
Row {
width: parent.width
spacing: Theme.spacingM
// Left icon
Text {
text: slider.leftIcon
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
visible: slider.leftIcon.length > 0
}
// Slider track
Rectangle {
id: sliderTrack
width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
height: 6
radius: 3
color: slider.enabled ?
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
anchors.verticalCenter: parent.verticalCenter
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
// Fill
Rectangle {
id: sliderFill
width: parent.width * ((slider.value - slider.minimum) / (slider.maximum - slider.minimum))
height: parent.height
radius: parent.radius
color: slider.enabled ? Theme.primary : Theme.surfaceVariantText
Behavior on width {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
}
// Draggable handle
Rectangle {
id: sliderHandle
width: 18
height: 18
radius: 9
color: slider.enabled ? Theme.primary : Theme.surfaceVariantText
border.color: slider.enabled ? Qt.lighter(Theme.primary, 1.3) : Qt.lighter(Theme.surfaceVariantText, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, sliderFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter
scale: sliderMouseArea.containsMouse || sliderMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
// Handle glow effect when active
Rectangle {
anchors.centerIn: parent
width: parent.width + 4
height: parent.height + 4
radius: width / 2
color: "transparent"
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
visible: sliderMouseArea.containsMouse && slider.enabled
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
}
MouseArea {
id: sliderMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: slider.enabled
onClicked: (mouse) => {
if (slider.enabled) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
}
}
onPositionChanged: (mouse) => {
if (pressed && slider.enabled) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
}
}
}
}
// Right icon
Text {
text: slider.rightIcon
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
visible: slider.rightIcon.length > 0
}
}
}
}

View File

@@ -0,0 +1,354 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import "../Common"
PanelWindow {
id: notificationHistoryPopup
visible: root.notificationHistoryVisible
implicitWidth: 400
implicitHeight: 500
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
width: 400
height: 500
x: parent.width - width - Theme.spacingL
y: Theme.barHeight + Theme.spacingS
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: root.notificationHistoryVisible ? 1.0 : 0.0
scale: root.notificationHistoryVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
// Header
Column {
width: parent.width
spacing: Theme.spacingM
Row {
width: parent.width
height: 32
Text {
text: "Notifications"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 200; height: 1 }
}
Rectangle {
width: parent.width
height: 36
radius: Theme.cornerRadius
color: clearArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.16) : Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
border.color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5)
border.width: 1
visible: notificationHistory.count > 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
Text {
text: "delete_sweep"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeSmall + 2
color: Theme.error
font.weight: Theme.iconFontWeight
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Clear All Notifications"
font.pixelSize: Theme.fontSizeMedium
color: Theme.error
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
notificationHistory.clear()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
// Notification List
ScrollView {
width: parent.width
height: parent.height - 120
clip: true
ListView {
id: notificationListView
model: notificationHistory
spacing: Theme.spacingS
delegate: Rectangle {
width: notificationListView.width
height: 80
radius: Theme.cornerRadius
color: notifArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
// Notification icon using reference pattern
Rectangle {
width: 32
height: 32
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
// Fallback material icon when no app icon
Loader {
active: !model.appIcon || model.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
text: model.appName ? model.appName.charAt(0).toUpperCase() : "notifications"
font.family: model.appName ? "Roboto" : Theme.iconFont
font.pixelSize: model.appName ? Theme.fontSizeMedium : 16
color: Theme.primary
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
// App icon when no notification image
Loader {
active: model.appIcon && model.appIcon !== "" && (!model.image || model.image === "")
anchors.fill: parent
anchors.margins: 3
sourceComponent: IconImage {
anchors.fill: parent
anchors.margins: 4
asynchronous: true
source: {
if (!model.appIcon) return ""
// Skip file:// URLs as they're usually screenshots/images, not icons
if (model.appIcon.startsWith("file://")) return ""
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
}
// Notification image with rounded corners
Loader {
active: model.image && model.image !== ""
anchors.fill: parent
sourceComponent: Item {
anchors.fill: parent
Image {
id: historyNotifImage
anchors.fill: parent
source: model.image || ""
fillMode: Image.PreserveAspectCrop
cache: false
antialiasing: true
asynchronous: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: historyNotifImage.width
height: historyNotifImage.height
radius: Theme.cornerRadius
}
}
}
// Small app icon overlay when showing notification image
Loader {
active: model.appIcon && model.appIcon !== ""
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
sourceComponent: IconImage {
width: 12
height: 12
asynchronous: true
source: model.appIcon ? Quickshell.iconPath(model.appIcon, "image-missing") : ""
}
}
}
}
}
// Content
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
spacing: Theme.spacingXS
Text {
text: model.appName || "App"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
Text {
text: model.summary || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
Text {
text: model.body || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
}
}
MouseArea {
id: notifArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
notificationHistory.remove(index)
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
// Empty state - properly centered
Rectangle {
anchors.fill: parent
visible: notificationHistory.count === 0
color: "transparent"
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
width: parent.width * 0.8
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "notifications_none"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge + 16
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
font.weight: Theme.iconFontWeight
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "No notifications"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Notifications will appear here"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
}
}
// Click outside to close
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.notificationHistoryVisible = false
}
}
}

View File

@@ -0,0 +1,203 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import "../Common"
PanelWindow {
id: notificationPopup
visible: root.showNotificationPopup && root.activeNotification
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
right: true
bottom: true
}
implicitWidth: 400
Rectangle {
id: popupContainer
width: 380
height: 100
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Theme.barHeight + 16
anchors.rightMargin: 16
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
opacity: root.showNotificationPopup ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: 200; easing.type: Easing.OutQuad }
}
MouseArea {
anchors.fill: parent
onClicked: Utils.hideNotificationPopup()
}
// Close button with cursor pointer
Text {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
text: "×"
font.pixelSize: 16
color: Theme.surfaceText
MouseArea {
anchors.fill: parent
anchors.margins: -4
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Utils.hideNotificationPopup()
}
}
// Content layout
Row {
anchors.fill: parent
anchors.margins: 12
anchors.rightMargin: 32
spacing: 12
// Notification icon using reference pattern
Rectangle {
width: 40
height: 40
radius: 8
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.verticalCenter: parent.verticalCenter
// Fallback material icon when no app icon
Loader {
active: !root.activeNotification || root.activeNotification.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
text: "notifications"
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.primary
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
// App icon when no notification image
Loader {
active: root.activeNotification && root.activeNotification.appIcon !== "" && (root.activeNotification.image === "" || !root.activeNotification.image)
anchors.fill: parent
anchors.margins: 4
sourceComponent: IconImage {
anchors.fill: parent
asynchronous: true
source: {
if (!root.activeNotification) return ""
let iconPath = root.activeNotification.appIcon
// Skip file:// URLs as they're usually screenshots/images, not icons
if (iconPath && iconPath.startsWith("file://")) return ""
return iconPath ? Quickshell.iconPath(iconPath, "image-missing") : ""
}
}
}
// Notification image with rounded corners
Loader {
active: root.activeNotification && root.activeNotification.image !== ""
anchors.fill: parent
sourceComponent: Item {
anchors.fill: parent
clip: true
Rectangle {
anchors.fill: parent
radius: 8
color: "transparent"
clip: true
Image {
id: notifImage
anchors.fill: parent
source: root.activeNotification ? root.activeNotification.image : ""
fillMode: Image.PreserveAspectCrop
cache: false
antialiasing: true
asynchronous: true
smooth: true
// Ensure minimum size and proper scaling
sourceSize.width: 64
sourceSize.height: 64
onStatusChanged: {
if (status === Image.Error) {
console.warn("Failed to load notification image:", source)
} else if (status === Image.Ready) {
console.log("Notification image loaded:", source, "size:", sourceSize)
}
}
}
}
// Small app icon overlay when showing notification image
Loader {
active: root.activeNotification && root.activeNotification.appIcon !== ""
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
sourceComponent: IconImage {
width: 16
height: 16
asynchronous: true
source: root.activeNotification ? Quickshell.iconPath(root.activeNotification.appIcon, "image-missing") : ""
}
}
}
}
}
// Text content
Column {
width: parent.width - 52
anchors.verticalCenter: parent.verticalCenter
spacing: 4
Text {
text: root.activeNotification ? (root.activeNotification.summary || "") : ""
font.pixelSize: 14
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
Text {
text: root.activeNotification ? (root.activeNotification.body || "") : ""
font.pixelSize: 12
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
}
}
}
}

View File

@@ -1,16 +1,25 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Services.Notifications
import Quickshell.Services.Mpris
import "../Common"
import "../Services"
PanelWindow {
id: topBar
property var theme
property var root
// modelData contains the screen from Quickshell.screens
property var modelData
screen: modelData
// Get the screen name (e.g., "DP-1", "DP-2")
property string screenName: modelData.name
anchors {
top: true
@@ -18,100 +27,779 @@ PanelWindow {
right: true
}
implicitHeight: theme.barHeight
implicitHeight: Theme.barHeight
color: "transparent"
Rectangle {
anchors.fill: parent
color: Qt.rgba(theme.surfaceContainer.r, theme.surfaceContainer.g, theme.surfaceContainer.b, 0.95)
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
border.width: 1
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.08)
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
NumberAnimation {
to: 0.12
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.06
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
}
Rectangle {
Item {
anchors.fill: parent
color: Qt.rgba(theme.surfaceTint.r, theme.surfaceTint.g, theme.surfaceTint.b, 0.08)
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
NumberAnimation {
to: 0.12
duration: theme.extraLongDuration
easing.type: theme.standardEasing
Row {
id: leftSection
height: parent.height
spacing: Theme.spacingL
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: archLauncher
width: Math.max(120, launcherRow.implicitWidth + Theme.spacingM * 2)
height: 32
radius: Theme.cornerRadius
color: launcherArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Row {
id: launcherRow
anchors.centerIn: parent
spacing: Theme.spacingS
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.osLogo || "apps" // Use OS logo if detected, fallback to apps icon
font.family: root.osLogo ? "NerdFont" : Theme.iconFont
font.pixelSize: root.osLogo ? Theme.iconSize - 2 : Theme.iconSize - 2
font.weight: Theme.iconFontWeight
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.isSmallScreen ? "Apps" : "Applications"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
visible: !root.isSmallScreen || width > 60
}
}
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
appLauncher.toggle()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
NumberAnimation {
to: 0.06
duration: theme.extraLongDuration
easing.type: theme.standardEasing
Rectangle {
id: workspaceSwitcher
width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
height: 32
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.8)
anchors.verticalCenter: parent.verticalCenter
property int currentWorkspace: 1
property var workspaceList: []
Process {
id: workspaceQuery
command: ["niri", "msg", "workspaces"]
running: true
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
workspaceSwitcher.parseWorkspaceOutput(text.trim())
}
}
}
}
function parseWorkspaceOutput(data) {
const lines = data.split('\n')
let currentOutputName = ""
let focusedOutput = ""
let focusedWorkspace = 1
let outputWorkspaces = {}
for (const line of lines) {
if (line.startsWith('Output "')) {
const outputMatch = line.match(/Output "(.+)"/)
if (outputMatch) {
currentOutputName = outputMatch[1]
outputWorkspaces[currentOutputName] = []
}
continue
}
if (line.trim() && line.match(/^\s*\*?\s*(\d+)$/)) {
const wsMatch = line.match(/^\s*(\*?)\s*(\d+)$/)
if (wsMatch) {
const isActive = wsMatch[1] === '*'
const wsNum = parseInt(wsMatch[2])
if (currentOutputName && outputWorkspaces[currentOutputName]) {
outputWorkspaces[currentOutputName].push(wsNum)
}
if (isActive) {
focusedOutput = currentOutputName
focusedWorkspace = wsNum
}
}
}
}
// Show workspaces for THIS screen only
if (topBar.screenName && outputWorkspaces[topBar.screenName]) {
workspaceList = outputWorkspaces[topBar.screenName]
// Always track the active workspace for this display
// Parse all lines to find which workspace is active on this display
let thisDisplayActiveWorkspace = 1
let inThisOutput = false
for (const line of lines) {
if (line.startsWith('Output "')) {
const outputMatch = line.match(/Output "(.+)"/)
inThisOutput = outputMatch && outputMatch[1] === topBar.screenName
continue
}
if (inThisOutput && line.trim() && line.match(/^\s*\*\s*(\d+)$/)) {
const wsMatch = line.match(/^\s*\*\s*(\d+)$/)
if (wsMatch) {
thisDisplayActiveWorkspace = parseInt(wsMatch[1])
break
}
}
}
currentWorkspace = thisDisplayActiveWorkspace
// console.log("Monitor", topBar.screenName, "active workspace:", thisDisplayActiveWorkspace)
} else {
// Fallback if screen name not found
workspaceList = [1, 2]
currentWorkspace = 1
}
}
Timer {
interval: 500
running: true
repeat: true
onTriggered: {
workspaceQuery.running = true
}
}
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
Repeater {
model: workspaceSwitcher.workspaceList
Rectangle {
property bool isActive: modelData === workspaceSwitcher.currentWorkspace
property bool isHovered: mouseArea.containsMouse
width: isActive ? Theme.spacingXL + Theme.spacingS : Theme.spacingL
height: Theme.spacingS
radius: height / 2
color: isActive ? Theme.primary :
isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Set target workspace and focus monitor first
console.log("Clicking workspace", modelData, "on monitor", topBar.screenName)
workspaceSwitcher.targetWorkspace = modelData
focusMonitorProcess.command = ["niri", "msg", "action", "focus-monitor", topBar.screenName]
focusMonitorProcess.running = true
}
}
}
}
}
Process {
id: switchProcess
running: false
onExited: {
// Update current workspace and refresh query
workspaceSwitcher.currentWorkspace = workspaceSwitcher.targetWorkspace
Qt.callLater(() => {
workspaceQuery.running = true
})
}
}
Process {
id: focusMonitorProcess
running: false
onExited: {
// After focusing the monitor, switch to the workspace
Qt.callLater(() => {
switchProcess.command = ["niri", "msg", "action", "focus-workspace", workspaceSwitcher.targetWorkspace.toString()]
switchProcess.running = true
})
}
}
property int targetWorkspace: 1
}
}
Rectangle {
id: clockContainer
width: {
let baseWidth = 200
if (root.hasActiveMedia) {
// Calculate width needed for media info + time/date + spacing + padding
let mediaWidth = 24 + Theme.spacingXS + mediaTitleText.implicitWidth + Theme.spacingM + 180
return Math.min(Math.max(mediaWidth, 300), parent.width - Theme.spacingL * 2)
} else if (root.weather.available) {
return Math.min(280, parent.width - Theme.spacingL * 2)
} else {
return Math.min(baseWidth, parent.width - Theme.spacingL * 2)
}
}
height: 32
radius: Theme.cornerRadius
color: clockMouseArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
anchors.centerIn: parent
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
property date currentDate: new Date()
Row {
anchors.centerIn: parent
spacing: Theme.spacingM
// Media info or Weather info
Row {
spacing: Theme.spacingXS
visible: root.hasActiveMedia || root.weather.available
anchors.verticalCenter: parent.verticalCenter
// Music icon when media is playing
Text {
text: "music_note"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia
SequentialAnimation on scale {
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
loops: Animation.Infinite
NumberAnimation { to: 1.1; duration: 500 }
NumberAnimation { to: 1.0; duration: 500 }
}
}
// Song title when media is playing
Text {
id: mediaTitleText
text: root.activePlayer?.trackTitle || "Unknown Track"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia
width: Math.min(implicitWidth, clockContainer.width - 100)
elide: Text.ElideRight
}
// Weather icon when no media but weather available
Text {
text: root.weatherIcons[root.weather.wCode] || "clear_day"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 2
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !root.hasActiveMedia && root.weather.available
}
// Weather temp when no media but weather available
Text {
text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
visible: !root.hasActiveMedia && root.weather.available
}
}
// Separator
Text {
text: "•"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia || root.weather.available
}
// Time and date
Row {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Text {
text: Qt.formatTime(clockContainer.currentDate, "h:mm AP")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "•"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: Qt.formatDate(clockContainer.currentDate, "ddd d")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
clockContainer.currentDate = new Date()
}
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.calendarVisible = !root.calendarVisible
}
}
}
Row {
id: rightSection
height: parent.height
spacing: Theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: Math.max(40, systemTrayRow.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
visible: systemTrayRow.children.length > 0
Row {
id: systemTrayRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: SystemTray.items
delegate: Rectangle {
width: 24
height: 24
radius: Theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
property var trayItem: modelData
Image {
anchors.centerIn: parent
width: 18
height: 18
source: {
let icon = trayItem?.icon || "";
if (!icon) return "";
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
asynchronous: true
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem) return;
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu) {
trayItem.activate()
}
} else if (mouse.button === Qt.RightButton) {
if (trayItem.hasMenu) {
console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown")
customTrayMenu.showMenu(mouse.x, mouse.y)
} else {
console.log("No menu available for:", trayItem.title || "Unknown")
}
}
}
}
// Custom Material 3 styled menu
QtObject {
id: customTrayMenu
property bool menuVisible: false
function showMenu(x, y) {
root.currentTrayMenu = customTrayMenu
root.currentTrayItem = trayItem
// Simple positioning: right side of screen, below the panel
root.trayMenuX = rightSection.x + rightSection.width - 180 - Theme.spacingL
root.trayMenuY = Theme.barHeight + Theme.spacingS
console.log("Showing menu at:", root.trayMenuX, root.trayMenuY)
menuVisible = true
root.showTrayMenu = true
}
function hideMenu() {
menuVisible = false
root.showTrayMenu = false
root.currentTrayMenu = null
root.currentTrayItem = null
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
// Clipboard History Button
Rectangle {
width: 40
height: 32
radius: Theme.cornerRadius
color: clipboardArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "content_paste" // Material icon for clipboard
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: Theme.surfaceText
}
MouseArea {
id: clipboardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
clipboardHistoryPopup.toggle()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Color Picker Button
Rectangle {
width: 40
height: 32
radius: Theme.cornerRadius
color: colorPickerArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "colorize" // Material icon for color picker
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: Theme.surfaceText
}
MouseArea {
id: colorPickerArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
ColorPickerService.pickColor()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Notification Center Button
Rectangle {
width: 40
height: 32
radius: Theme.cornerRadius
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
property bool hasUnread: notificationHistory.count > 0
Text {
anchors.centerIn: parent
text: "notifications" // Material icon for notifications
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
Theme.primary : Theme.surfaceText
}
// Notification dot indicator
Rectangle {
width: 8
height: 8
radius: 4
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
visible: parent.hasUnread
}
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.notificationHistoryVisible = !root.notificationHistoryVisible
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Control Center Indicators
Rectangle {
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: controlCenterArea.containsMouse || root.controlCenterVisible ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Row {
id: controlIndicators
anchors.centerIn: parent
spacing: Theme.spacingXS
// Network Status Icon
Text {
text: {
if (root.networkStatus === "ethernet") return "lan"
else if (root.networkStatus === "wifi") {
switch (root.wifiSignalStrength) {
case "excellent": return "wifi"
case "good": return "wifi_2_bar"
case "fair": return "wifi_1_bar"
case "poor": return "wifi_calling_3"
default: return "wifi"
}
}
else return "wifi_off"
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: root.networkStatus !== "disconnected" ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: true
}
// Audio Icon
Text {
text: root.volumeLevel === 0 ? "volume_off" :
root.volumeLevel < 33 ? "volume_down" : "volume_up"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: controlCenterArea.containsMouse || root.controlCenterVisible ?
Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
// Microphone Icon (when active)
Text {
text: "mic"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: false // TODO: Add mic detection
}
// Bluetooth Icon (when available and enabled)
Text {
text: "bluetooth"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: root.bluetoothEnabled ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: root.bluetoothAvailable && root.bluetoothEnabled
}
}
MouseArea {
id: controlCenterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.controlCenterVisible = !root.controlCenterVisible
if (root.controlCenterVisible) {
// Refresh data when opening control center
WifiService.scanWifi()
BluetoothService.scanDevices()
// Audio sink info is automatically refreshed by AudioService
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: theme.spacingL
anchors.rightMargin: theme.spacingL
// Left section - Apps and Workspace Switcher
Row {
id: leftSection
height: parent.height
spacing: theme.spacingL
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
AppLauncherButton {
theme: topBar.theme
root: topBar.root
}
WorkspaceSwitcher {
theme: topBar.theme
root: topBar.root
}
}
// Center section - Clock/Media Player
ClockWidget {
id: clockWidget
theme: topBar.theme
root: topBar.root
anchors.centerIn: parent
}
// Right section - System controls
Row {
id: rightSection
height: parent.height
spacing: theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
SystemTrayWidget {
theme: topBar.theme
root: topBar.root
}
ClipboardButton {
theme: topBar.theme
root: topBar.root
}
ColorPickerButton {
theme: topBar.theme
root: topBar.root
}
NotificationButton {
theme: topBar.theme
root: topBar.root
}
}
}
}

154
Widgets/TrayMenuPopup.qml Normal file
View File

@@ -0,0 +1,154 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import "../Common"
PanelWindow {
id: trayMenuPopup
visible: root.showTrayMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: menuContainer
x: root.trayMenuX
y: root.trayMenuY
width: 180
height: Math.max(60, menuList.contentHeight + Theme.spacingS * 2)
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
// Material 3 drop shadow
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
// Material 3 animations
opacity: root.showTrayMenu ? 1.0 : 0.0
scale: root.showTrayMenu ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
QsMenuOpener {
id: menuOpener
menu: root.currentTrayItem?.menu
}
// Custom menu styling using ListView
ListView {
id: menuList
anchors.fill: parent
spacing: 1
model: ScriptModel {
values: menuOpener.children ? [...menuOpener.children.values].filter(item => {
// Filter out empty items and separators
return item && item.text && item.text.trim().length > 0 && !item.isSeparator
}) : []
}
delegate: Rectangle {
width: ListView.view.width
height: modelData.isSeparator ? 5 : 28
radius: modelData.isSeparator ? 0 : Theme.cornerRadiusSmall
color: modelData.isSeparator ? "transparent" :
(menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent")
// Separator line
Rectangle {
visible: modelData.isSeparator
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
// Menu item content
Row {
visible: !modelData.isSeparator
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Text {
text: modelData.text || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
}
MouseArea {
id: menuItemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.isSeparator ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !modelData.isSeparator
onClicked: {
if (modelData.triggered) {
modelData.triggered()
}
root.showTrayMenu = false
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
// Click outside to close
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.showTrayMenu = false
}
}
}

View File

@@ -0,0 +1,308 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import "../Common"
import "../Services"
PanelWindow {
id: wifiPasswordDialog
visible: root.wifiPasswordDialogVisible
anchors {
top: true
left: true
right: true
bottom: true
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: root.wifiPasswordDialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
onVisibleChanged: {
if (visible) {
passwordInput.forceActiveFocus()
}
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: root.wifiPasswordDialogVisible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
MouseArea {
anchors.fill: parent
onClicked: {
root.wifiPasswordDialogVisible = false
root.wifiPasswordInput = ""
}
}
}
Rectangle {
width: Math.min(400, parent.width - Theme.spacingL * 2)
height: Math.min(250, parent.height - Theme.spacingL * 2)
anchors.centerIn: parent
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: root.wifiPasswordDialogVisible ? 1.0 : 0.0
scale: root.wifiPasswordDialogVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
// Header
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
Text {
text: "Connect to Wi-Fi"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: "Enter password for \"" + root.wifiPasswordSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: closeDialogArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "close"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4
color: closeDialogArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeDialogArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.wifiPasswordDialogVisible = false
root.wifiPasswordInput = ""
}
}
}
}
// Password input
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
border.color: passwordInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: passwordInput.activeFocus ? 2 : 1
TextInput {
id: passwordInput
anchors.fill: parent
anchors.margins: Theme.spacingM
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
verticalAlignment: TextInput.AlignVCenter
cursorVisible: activeFocus
selectByMouse: true
Text {
anchors.fill: parent
text: "Enter password"
font: parent.font
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
verticalAlignment: Text.AlignVCenter
visible: parent.text.length === 0
}
onTextChanged: {
root.wifiPasswordInput = text
}
onAccepted: {
WifiService.connectToWifiWithPassword(root.wifiPasswordSSID, root.wifiPasswordInput)
}
Component.onCompleted: {
if (root.wifiPasswordDialogVisible) {
forceActiveFocus()
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: {
passwordInput.forceActiveFocus()
}
}
}
// Show password checkbox
Row {
spacing: Theme.spacingS
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
border.width: 2
Text {
anchors.centerIn: parent
text: "check"
font.family: Theme.iconFont
font.pixelSize: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
}
}
}
Text {
text: "Show password"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
// Buttons
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
Text {
id: cancelText
anchors.centerIn: parent
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.wifiPasswordDialogVisible = false
root.wifiPasswordInput = ""
}
}
}
Rectangle {
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: root.wifiPasswordInput.length > 0
opacity: enabled ? 1.0 : 0.5
Text {
id: connectText
anchors.centerIn: parent
text: "Connect"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
WifiService.connectToWifiWithPassword(root.wifiPasswordSSID, root.wifiPasswordInput)
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}

View File

@@ -6,4 +6,13 @@ ClockWidget 1.0 ClockWidget.qml
SystemTrayWidget 1.0 SystemTrayWidget.qml
ClipboardButton 1.0 ClipboardButton.qml
ColorPickerButton 1.0 ColorPickerButton.qml
NotificationButton 1.0 NotificationButton.qml
NotificationButton 1.0 NotificationButton.qml
CalendarPopup 1.0 CalendarPopup.qml
TrayMenuPopup 1.0 TrayMenuPopup.qml
NotificationPopup 1.0 NotificationPopup.qml
NotificationHistoryPopup 1.0 NotificationHistoryPopup.qml
ControlCenterPopup 1.0 ControlCenterPopup.qml
WifiPasswordDialog 1.0 WifiPasswordDialog.qml
AppLauncher 1.0 AppLauncher.qml
ClipboardHistory 1.0 ClipboardHistory.qml
CustomSlider 1.0 CustomSlider.qml