1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

feat: Wallpapers built into the Media Hub

- Thanks @TaylanTatli for the inspiration
This commit is contained in:
purian23
2025-10-21 23:09:10 -04:00
parent 2a89885437
commit 647c358b72
46 changed files with 7271 additions and 2401 deletions

View File

@@ -404,29 +404,16 @@ DankPopout {
width: appList.iconSize
height: appList.iconSize
anchors.verticalCenter: parent.verticalCenter
visible: model.icon !== undefined && model.icon !== ""
property string iconValue: model.icon || ""
property bool isMaterial: iconValue.indexOf("material:") === 0
property string materialName: isMaterial ? iconValue.substring(9) : ""
DankIcon {
anchors.centerIn: parent
name: parent.materialName
size: appList.iconSize - Theme.spacingM
color: Theme.surfaceText
visible: parent.isMaterial
}
IconImage {
id: listIconImg
anchors.fill: parent
anchors.margins: Theme.spacingXS
source: parent.isMaterial ? "" : Quickshell.iconPath(parent.iconValue, true)
source: Quickshell.iconPath(model.icon, true)
smooth: true
asynchronous: true
visible: !parent.isMaterial && status === Image.Ready
visible: status === Image.Ready
}
Rectangle {
@@ -434,7 +421,7 @@ DankPopout {
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingM
visible: !parent.isMaterial && listIconImg.status !== Image.Ready
visible: !listIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 0
@@ -448,12 +435,11 @@ DankPopout {
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: (model.icon !== undefined && model.icon !== "") ? (parent.width - appList.iconSize - Theme.spacingL) : parent.width
width: parent.width - appList.iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
@@ -527,7 +513,6 @@ DankPopout {
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset
@@ -593,19 +578,6 @@ DankPopout {
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
visible: model.icon !== undefined && model.icon !== ""
property string iconValue: model.icon || ""
property bool isMaterial: iconValue.indexOf("material:") === 0
property string materialName: isMaterial ? iconValue.substring(9) : ""
DankIcon {
anchors.centerIn: parent
name: parent.materialName
size: parent.iconSize - Theme.spacingL
color: Theme.surfaceText
visible: parent.isMaterial
}
IconImage {
id: gridIconImg
@@ -614,10 +586,10 @@ DankPopout {
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
source: parent.isMaterial ? "" : Quickshell.iconPath(parent.iconValue, true)
source: Quickshell.iconPath(model.icon, true)
smooth: true
asynchronous: true
visible: !parent.isMaterial && status === Image.Ready
visible: status === Image.Ready
}
Rectangle {
@@ -625,7 +597,7 @@ DankPopout {
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: !parent.isMaterial && gridIconImg.status !== Image.Ready
visible: !gridIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 0

View File

@@ -8,10 +8,6 @@ import qs.Widgets
Item {
id: root
// DEVELOPER NOTE: This component manages the AppDrawer launcher (accessed via DankBar icon).
// Changes to launcher behavior, especially item rendering, filtering, or model structure,
// likely require corresponding updates in Modals/Spotlight/SpotlightResults.qml and vice versa.
property string searchQuery: ""
property string selectedCategory: I18n.tr("All")
property string viewMode: "list" // "list" or "grid"
@@ -167,7 +163,7 @@ Item {
filteredModel.append({
"name": app.name || "",
"exec": app.execString || app.exec || app.action || "",
"icon": app.icon !== undefined ? app.icon : (isPluginItem ? "" : "application-x-executable"),
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"isPlugin": isPluginItem,

View File

@@ -509,11 +509,7 @@ Rectangle {
onClicked: function(event) {
if (modelData.ssid !== NetworkService.currentWifiSSID) {
if (modelData.secured && !modelData.saved) {
if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(modelData.ssid)
} else if (PopoutService.wifiPasswordModal) {
PopoutService.wifiPasswordModal.show(modelData.ssid)
}
wifiPasswordModal.show(modelData.ssid)
} else {
NetworkService.connectToWifi(modelData.ssid)
}
@@ -567,11 +563,7 @@ Rectangle {
NetworkService.disconnectWifi()
} else {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(networkContextMenu.currentSSID)
} else if (PopoutService.wifiPasswordModal) {
PopoutService.wifiPasswordModal.show(networkContextMenu.currentSSID)
}
wifiPasswordModal.show(networkContextMenu.currentSSID)
} else {
NetworkService.connectToWifi(networkContextMenu.currentSSID)
}
@@ -626,6 +618,10 @@ Rectangle {
}
}
WifiPasswordModal {
id: wifiPasswordModal
}
NetworkInfoModal {
id: networkInfoModal
}

View File

@@ -23,7 +23,6 @@ Item {
signal colorPickerRequested
property alias barVariants: barVariants
property var hyprlandOverviewLoader: null
function triggerControlCenterOnFocusedScreen() {
let focusedScreenName = ""
@@ -49,6 +48,30 @@ Item {
return false
}
function triggerWallpaperBrowserOnFocusedScreen() {
let focusedScreenName = ""
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
focusedScreenName = Hyprland.focusedWorkspace.monitor.name
} else if (CompositorService.isNiri && NiriService.currentOutput) {
focusedScreenName = NiriService.currentOutput
}
if (!focusedScreenName && barVariants.instances.length > 0) {
const firstBar = barVariants.instances[0]
firstBar.triggerWallpaperBrowser()
return true
}
for (var i = 0; i < barVariants.instances.length; i++) {
const barInstance = barVariants.instances[i]
if (barInstance.modelData && barInstance.modelData.name === focusedScreenName) {
barInstance.triggerWallpaperBrowser()
return true
}
}
return false
}
Variants {
id: barVariants
model: SettingsData.getFilteredScreens("dankBar")
@@ -57,6 +80,7 @@ Item {
id: barWindow
property var controlCenterButtonRef: null
property var clockButtonRef: null
function triggerControlCenter() {
controlCenterLoader.active = true
@@ -79,6 +103,27 @@ Item {
}
}
function triggerWallpaperBrowser() {
dankDashPopoutLoader.active = true
if (!dankDashPopoutLoader.item) {
return
}
if (clockButtonRef && dankDashPopoutLoader.item.setTriggerPosition) {
const globalPos = clockButtonRef.mapToGlobal(0, 0)
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, clockButtonRef.width)
const section = clockButtonRef.section || "center"
dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen)
} else {
dankDashPopoutLoader.item.triggerScreen = barWindow.screen
}
if (!dankDashPopoutLoader.item.dashVisible) {
dankDashPopoutLoader.item.currentTabIndex = 2
}
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible
}
readonly property var dBarLayer: {
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
case "bottom":
@@ -773,7 +818,6 @@ Item {
section: topBarContent.getWidgetSection(parent)
popupTarget: appDrawerLoader.item
parentScreen: barWindow.screen
hyprlandOverviewLoader: root.hyprlandOverviewLoader
onClicked: {
appDrawerLoader.active = true
appDrawerLoader.item?.toggle()
@@ -787,7 +831,6 @@ Item {
WorkspaceSwitcher {
screenName: barWindow.screenName
widgetHeight: barWindow.widgetThickness
hyprlandOverviewLoader: root.hyprlandOverviewLoader
}
}
@@ -825,6 +868,17 @@ Item {
return dankDashPopoutLoader.item
}
parentScreen: barWindow.screen
Component.onCompleted: {
barWindow.clockButtonRef = this
}
Component.onDestruction: {
if (barWindow.clockButtonRef === this) {
barWindow.clockButtonRef = null
}
}
onClockClicked: {
dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) {
@@ -874,7 +928,7 @@ Item {
dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) {
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible
dankDashPopoutLoader.item.currentTabIndex = 2
dankDashPopoutLoader.item.currentTabIndex = 3
}
}
}
@@ -1208,4 +1262,15 @@ Item {
}
}
}
IpcHandler {
target: "dankdash"
function wallpaper(): string {
if (root.triggerWallpaperBrowserOnFocusedScreen()) {
return "SUCCESS: Toggled wallpaper browser"
}
return "ERROR: Failed to toggle wallpaper browser"
}
}
}

View File

@@ -74,20 +74,14 @@ Rectangle {
return false
}
try {
if (!Hyprland.toplevels) return false
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
const activeHyprToplevel = hyprlandToplevels.find(t => t?.wayland === activeWindow)
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
const activeHyprToplevel = hyprlandToplevels.find(t => t.wayland === activeWindow)
if (!activeHyprToplevel || !activeHyprToplevel.workspace) {
return false
}
return activeHyprToplevel.workspace.id === Hyprland.focusedWorkspace.id
} catch (e) {
console.error("FocusedApp: hasWindowsOnCurrentWorkspace error:", e)
if (!activeHyprToplevel || !activeHyprToplevel.workspace) {
return false
}
return activeHyprToplevel.workspace.id === Hyprland.focusedWorkspace.id
}
return activeWindow && activeWindow.title

View File

@@ -17,7 +17,6 @@ Item {
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property var hyprlandOverviewLoader: null
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
@@ -36,8 +35,6 @@ Item {
if (mouse.button === Qt.RightButton) {
if (CompositorService.isNiri) {
NiriService.toggleOverview()
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
}
return
}

View File

@@ -30,28 +30,22 @@ Rectangle {
if (!SettingsData.runningAppsGroupByApp) {
return [];
}
try {
const appGroups = new Map();
sortedToplevels.forEach((toplevel, index) => {
if (!toplevel) return;
const appId = toplevel?.appId || "unknown";
if (!appGroups.has(appId)) {
appGroups.set(appId, {
appId: appId,
windows: []
});
}
appGroups.get(appId).windows.push({
toplevel: toplevel,
windowId: index,
windowTitle: toplevel?.title || "(Unnamed)"
const appGroups = new Map();
sortedToplevels.forEach((toplevel, index) => {
const appId = toplevel.appId || "unknown";
if (!appGroups.has(appId)) {
appGroups.set(appId, {
appId: appId,
windows: []
});
}
appGroups.get(appId).windows.push({
toplevel: toplevel,
windowId: index,
windowTitle: toplevel.title || "(Unnamed)"
});
return Array.from(appGroups.values());
} catch (e) {
console.error("RunningApps: groupedWindows error:", e);
return [];
}
});
return Array.from(appGroups.values());
}
readonly property int windowCount: SettingsData.runningAppsGroupByApp ? groupedWindows.length : sortedToplevels.length
readonly property int calculatedSize: {

View File

@@ -15,7 +15,6 @@ Rectangle {
property string screenName: ""
property real widgetHeight: 30
property real barThickness: 48
property var hyprlandOverviewLoader: null
readonly property var sortedToplevels: {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen?.name);
}
@@ -245,17 +244,11 @@ Rectangle {
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
acceptedButtons: Qt.NoButton
property real scrollAccumulator: 0
property real touchpadThreshold: 500
onClicked: mouse => {
if (mouse.button === Qt.RightButton && CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
}
}
onWheel: wheel => {
const deltaY = wheel.angleDelta.y
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0

View File

@@ -16,6 +16,8 @@ DankPopout {
property var triggerScreen: null
property int currentTabIndex: 0
keyboardFocusMode: WlrKeyboardFocus.Exclusive
function setTriggerPosition(x, y, width, section, screen) {
triggerSection = section
triggerScreen = screen
@@ -43,15 +45,49 @@ DankPopout {
shouldBeVisible: dashVisible
visible: shouldBeVisible
property bool __focusArmed: false
property bool __contentReady: false
function __tryFocusOnce() {
if (!__focusArmed) return
const win = root.window
if (!win || !win.visible) return
if (!contentLoader.item) return
if (win.requestActivate) win.requestActivate()
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
if (contentLoader.item.activeFocus)
__focusArmed = false
}
onDashVisibleChanged: {
if (dashVisible) {
__focusArmed = true
__contentReady = !!contentLoader.item
open()
__tryFocusOnce()
} else {
__focusArmed = false
__contentReady = false
close()
}
}
Connections {
target: contentLoader
function onLoaded() {
__contentReady = true
if (__focusArmed) __tryFocusOnce()
}
}
Connections {
target: root.window ? root.window : null
enabled: !!root.window
function onVisibleChanged() { if (__focusArmed) __tryFocusOnce() }
}
onBackgroundClicked: {
dashVisible = false
}
@@ -67,18 +103,12 @@ DankPopout {
Component.onCompleted: {
if (root.shouldBeVisible) {
forceActiveFocus()
}
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
root.dashVisible = false
event.accepted = true
mainContainer.forceActiveFocus()
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function() {
@@ -86,7 +116,52 @@ DankPopout {
})
}
}
target: root
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
root.dashVisible = false
event.accepted = true
return
}
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
let nextIndex = root.currentTabIndex + 1
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
nextIndex++
}
if (nextIndex >= tabBar.model.length) {
nextIndex = 0
}
root.currentTabIndex = nextIndex
event.accepted = true
return
}
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
let prevIndex = root.currentTabIndex - 1
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
}
if (prevIndex < 0) {
prevIndex = tabBar.model.length - 1
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
}
}
if (prevIndex >= 0) {
root.currentTabIndex = prevIndex
}
event.accepted = true
return
}
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
if (wallpaperTab.handleKeyEvent(event)) {
event.accepted = true
return
}
}
}
Rectangle {
@@ -128,11 +203,23 @@ DankPopout {
currentIndex: root.currentTabIndex
spacing: Theme.spacingS
equalWidthTabs: true
enableArrowNavigation: false
focus: false
activeFocusOnTab: false
nextFocusTarget: {
const item = pages.currentItem
if (!item)
return null
if (item.focusTarget)
return item.focusTarget
return item
}
model: {
let tabs = [
{ icon: "dashboard", text: I18n.tr("Overview") },
{ icon: "music_note", text: I18n.tr("Media") }
{ icon: "music_note", text: I18n.tr("Media") },
{ icon: "wallpaper", text: I18n.tr("Wallpapers") }
]
if (SettingsData.weatherEnabled) {
@@ -148,7 +235,7 @@ DankPopout {
}
onActionTriggered: function(index) {
let settingsIndex = SettingsData.weatherEnabled ? 3 : 2
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
if (index === settingsIndex) {
dashVisible = false
settingsModal.show()
@@ -168,7 +255,8 @@ DankPopout {
implicitHeight: {
if (currentIndex === 0) return overviewTab.implicitHeight
if (currentIndex === 1) return mediaTab.implicitHeight
if (SettingsData.weatherEnabled && currentIndex === 2) return weatherTab.implicitHeight
if (currentIndex === 2) return wallpaperTab.implicitHeight
if (SettingsData.weatherEnabled && currentIndex === 3) return weatherTab.implicitHeight
return overviewTab.implicitHeight
}
currentIndex: root.currentTabIndex
@@ -178,8 +266,8 @@ DankPopout {
onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) {
tabBar.currentIndex = 2
tabBar.tabClicked(2)
tabBar.currentIndex = 3
tabBar.tabClicked(3)
}
}
@@ -193,9 +281,16 @@ DankPopout {
id: mediaTab
}
WallpaperTab {
id: wallpaperTab
active: root.currentTabIndex === 2
tabBarItem: tabBar
keyForwardTarget: mainContainer
}
WeatherTab {
id: weatherTab
visible: SettingsData.weatherEnabled && root.currentTabIndex === 2
visible: SettingsData.weatherEnabled && root.currentTabIndex === 3
}
}
}

View File

@@ -0,0 +1,523 @@
import Qt.labs.folderlistmodel
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
Item {
id: root
implicitWidth: 700
implicitHeight: 410
property var wallpaperList: []
property string wallpaperDir: ""
property int currentPage: 0
property int itemsPerPage: 16
property int totalPages: Math.max(1, Math.ceil(wallpaperList.length / itemsPerPage))
property bool active: false
property Item focusTarget: wallpaperGrid
property Item tabBarItem: null
property int gridIndex: 0
property Item keyForwardTarget: null
property int lastPage: 0
property bool enableAnimation: false
signal requestTabChange(int newIndex)
onCurrentPageChanged: {
if (currentPage !== lastPage) {
enableAnimation = false
lastPage = currentPage
}
}
onVisibleChanged: {
if (visible && active) {
setInitialSelection()
}
}
Component.onCompleted: {
loadWallpapers()
if (visible && active) {
setInitialSelection()
}
}
onActiveChanged: {
if (active && visible) {
setInitialSelection()
}
}
function handleKeyEvent(event) {
const columns = 4
const rows = 4
const currentRow = Math.floor(gridIndex / columns)
const currentCol = gridIndex % columns
const visibleCount = wallpaperGrid.model.length
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (gridIndex >= 0) {
const item = wallpaperGrid.currentItem
if (item && item.wallpaperPath) {
SessionData.setWallpaper(item.wallpaperPath)
}
}
return true
}
if (event.key === Qt.Key_Right) {
if (gridIndex + 1 < visibleCount) {
// Move right within current page
gridIndex++
} else if (gridIndex === visibleCount - 1 && currentPage < totalPages - 1) {
// At last item in page, go to next page
gridIndex = 0
currentPage++
}
return true
}
if (event.key === Qt.Key_Left) {
if (gridIndex > 0) {
// Move left within current page
gridIndex--
} else if (gridIndex === 0 && currentPage > 0) {
// At first item in page, go to previous page (last item)
currentPage--
gridIndex = Math.min(itemsPerPage - 1, wallpaperList.length - currentPage * itemsPerPage - 1)
}
return true
}
if (event.key === Qt.Key_Down) {
if (gridIndex + columns < visibleCount) {
// Move down within current page
gridIndex += columns
} else if (gridIndex >= visibleCount - columns && currentPage < totalPages - 1) {
// In last row, go to next page
gridIndex = currentCol
currentPage++
}
return true
}
if (event.key === Qt.Key_Up) {
if (gridIndex >= columns) {
// Move up within current page
gridIndex -= columns
} else if (gridIndex < columns && currentPage > 0) {
// In first row, go to previous page (last row)
currentPage--
const prevPageCount = Math.min(itemsPerPage, wallpaperList.length - currentPage * itemsPerPage)
const prevPageRows = Math.ceil(prevPageCount / columns)
gridIndex = (prevPageRows - 1) * columns + currentCol
gridIndex = Math.min(gridIndex, prevPageCount - 1)
}
return true
}
if (event.key === Qt.Key_PageUp && currentPage > 0) {
gridIndex = 0
currentPage--
return true
}
if (event.key === Qt.Key_PageDown && currentPage < totalPages - 1) {
gridIndex = 0
currentPage++
return true
}
if (event.key === Qt.Key_Home && event.modifiers & Qt.ControlModifier) {
gridIndex = 0
currentPage = 0
return true
}
if (event.key === Qt.Key_End && event.modifiers & Qt.ControlModifier) {
gridIndex = 0
currentPage = totalPages - 1
return true
}
return false
}
function setInitialSelection() {
if (!SessionData.wallpaperPath) {
gridIndex = 0
return
}
const startIndex = currentPage * itemsPerPage
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperList.length)
const pageWallpapers = wallpaperList.slice(startIndex, endIndex)
for (let i = 0; i < pageWallpapers.length; i++) {
if (pageWallpapers[i] === SessionData.wallpaperPath) {
gridIndex = i
return
}
}
gridIndex = 0
}
onWallpaperListChanged: {
if (visible && active) {
setInitialSelection()
}
}
function loadWallpapers() {
const currentWallpaper = SessionData.wallpaperPath
// Try current wallpaper path / fallback to wallpaperLastPath
if (!currentWallpaper || currentWallpaper.startsWith("#") || currentWallpaper.startsWith("we:")) {
if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") {
wallpaperDir = CacheData.wallpaperLastPath
} else {
wallpaperDir = ""
wallpaperList = []
}
return
}
wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'))
}
function updateWallpaperList() {
if (!wallpaperFolderModel || wallpaperFolderModel.count === 0) {
wallpaperList = []
currentPage = 0
gridIndex = 0
return
}
// Build list from FolderListModel
const files = []
for (let i = 0; i < wallpaperFolderModel.count; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath")
if (filePath) {
// Remove file:// prefix if present
const cleanPath = filePath.toString().replace(/^file:\/\//, '')
files.push(cleanPath)
}
}
wallpaperList = files
const currentPath = SessionData.wallpaperPath
const selectedIndex = currentPath ? wallpaperList.indexOf(currentPath) : -1
if (selectedIndex >= 0) {
currentPage = Math.floor(selectedIndex / itemsPerPage)
gridIndex = selectedIndex % itemsPerPage
} else {
const maxPage = Math.max(0, Math.ceil(files.length / itemsPerPage) - 1)
currentPage = Math.min(Math.max(0, currentPage), maxPage)
gridIndex = 0
}
}
Connections {
target: SessionData
function onWallpaperPathChanged() {
loadWallpapers()
}
}
FolderListModel {
id: wallpaperFolderModel
showDirsFirst: false
showDotAndDotDot: false
showHidden: false
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
showFiles: true
showDirs: false
sortField: FolderListModel.Name
folder: wallpaperDir ? "file://" + wallpaperDir : ""
onStatusChanged: {
if (status === FolderListModel.Ready) {
updateWallpaperList()
}
}
onCountChanged: {
if (status === FolderListModel.Ready) {
updateWallpaperList()
}
}
}
Loader {
id: wallpaperBrowserLoader
active: false
asynchronous: true
sourceComponent: FileBrowserModal {
Component.onCompleted: {
open()
}
browserTitle: "Select Wallpaper Directory"
browserIcon: "folder_open"
browserType: "wallpaper"
showHiddenFiles: false
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
allowStacking: true
onFileSelected: (path) => {
// Set the selected wallpaper
const cleanPath = path.replace(/^file:\/\//, '')
SessionData.setWallpaper(cleanPath)
// Extract directory from the selected file and load all wallpapers
const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'))
if (dirPath) {
wallpaperDir = dirPath
CacheData.wallpaperLastPath = dirPath
CacheData.saveCache()
}
close()
}
onDialogClosed: {
Qt.callLater(() => wallpaperBrowserLoader.active = false)
}
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: parent.height - 50
GridView {
id: wallpaperGrid
anchors.centerIn: parent
width: parent.width - Theme.spacingS
height: parent.height - Theme.spacingS
cellWidth: width / 4
cellHeight: height / 4
clip: true
enabled: root.active
interactive: root.active
boundsBehavior: Flickable.StopAtBounds
keyNavigationEnabled: false
activeFocusOnTab: false
highlightFollowsCurrentItem: true
highlightMoveDuration: enableAnimation ? Theme.shortDuration : 0
focus: false
highlight: Item {
z: 1000
Rectangle {
anchors.fill: parent
anchors.margins: Theme.spacingXS
color: "transparent"
border.width: 3
border.color: Theme.primary
radius: Theme.cornerRadius
}
}
model: {
const startIndex = currentPage * itemsPerPage
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperList.length)
return wallpaperList.slice(startIndex, endIndex)
}
onModelChanged: {
const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0
if (gridIndex !== clampedIndex) {
gridIndex = clampedIndex
}
}
onCountChanged: {
if (count > 0) {
const clampedIndex = Math.min(gridIndex, count - 1)
currentIndex = clampedIndex
positionViewAtIndex(clampedIndex, GridView.Contain)
}
enableAnimation = true
}
Connections {
target: root
function onGridIndexChanged() {
if (enableAnimation && wallpaperGrid.count > 0) {
wallpaperGrid.currentIndex = gridIndex
}
}
}
delegate: Item {
width: wallpaperGrid.cellWidth
height: wallpaperGrid.cellHeight
property string wallpaperPath: modelData || ""
property bool isSelected: SessionData.wallpaperPath === modelData
Rectangle {
id: wallpaperCard
anchors.fill: parent
anchors.margins: Theme.spacingXS
color: Theme.surfaceContainerHighest
radius: Theme.cornerRadius
clip: true
Rectangle {
anchors.fill: parent
color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
radius: parent.radius
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Image {
id: thumbnailImage
anchors.fill: parent
source: modelData ? `file://${modelData}` : ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
smooth: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: thumbnailImage.width
height: thumbnailImage.height
radius: Theme.cornerRadius
}
}
}
}
BusyIndicator {
anchors.centerIn: parent
running: thumbnailImage.status === Image.Loading
visible: running
}
StateLayer {
anchors.fill: parent
cornerRadius: parent.radius
stateColor: Theme.primary
}
MouseArea {
id: wallpaperMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
gridIndex = index
if (modelData) {
SessionData.setWallpaper(modelData)
}
// Don't steal focus - let mainContainer keep it for keyboard nav
}
}
}
}
}
StyledText {
anchors.centerIn: parent
visible: wallpaperList.length === 0
text: "No wallpapers found\n\nClick the folder icon below to browse"
font.pixelSize: 14
color: Theme.outline
horizontalAlignment: Text.AlignHCenter
}
}
Row {
width: parent.width
height: 50
spacing: Theme.spacingS
Item {
width: (parent.width - controlsRow.width - browseButton.width - Theme.spacingS) / 2
height: parent.height
}
Row {
id: controlsRow
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "skip_previous"
iconSize: 20
buttonSize: 32
enabled: currentPage > 0
opacity: enabled ? 1.0 : 0.3
onClicked: {
if (currentPage > 0) {
currentPage--
}
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: wallpaperList.length > 0 ? `${wallpaperList.length} wallpapers ${currentPage + 1} / ${totalPages}` : "No wallpapers"
font.pixelSize: 14
color: Theme.surfaceText
opacity: 0.7
}
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "skip_next"
iconSize: 20
buttonSize: 32
enabled: currentPage < totalPages - 1
opacity: enabled ? 1.0 : 0.3
onClicked: {
if (currentPage < totalPages - 1) {
currentPage++
}
}
}
}
DankActionButton {
id: browseButton
anchors.verticalCenter: parent.verticalCenter
iconName: "folder_open"
iconSize: 20
buttonSize: 32
opacity: 0.7
onClicked: wallpaperBrowserLoader.active = true
}
}
}
}

View File

@@ -7,6 +7,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Greetd
import Quickshell.Services.Pam
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
@@ -15,6 +16,8 @@ import qs.Modules.Lock
Item {
id: root
required property var sessionLock
readonly property string xdgDataDirs: Quickshell.env("XDG_DATA_DIRS")
property string screenName: ""
property string randomFact: ""
@@ -114,6 +117,21 @@ Item {
onTriggered: updateHyprlandLayout()
}
// ! This was for development and testing, just leaving so people can see how I did it.
// Timer {
// id: autoUnlockTimer
// interval: 10000
// running: true
// onTriggered: {
// root.sessionLock.locked = false
// GreeterState.unlocking = true
// const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]
// if (sessionCmd) {
// GreetdMemory.setLastSessionId(sessionCmd.split(" ")[0])
// Greetd.launch(sessionCmd.split(" "), [], true)
// }
// }
// }
Connections {
target: GreetdMemory
@@ -655,11 +673,180 @@ Item {
height: 24
color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter
visible: {
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) ||
(CompositorService.isHyprland && hyprlandLayoutCount > 1)
return keyboardVisible && WeatherService.weather.available
visible: MprisController.activePlayer
}
Row {
spacing: Theme.spacingS
visible: MprisController.activePlayer
anchors.verticalCenter: parent.verticalCenter
Item {
width: 20
height: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
Loader {
active: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing
sourceComponent: Component {
Ref {
service: CavaService
}
}
}
Timer {
running: !CavaService.cavaAvailable && MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing
interval: 256
repeat: true
onTriggered: {
CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25]
}
}
Row {
anchors.centerIn: parent
spacing: 1.5
Repeater {
model: 6
Rectangle {
width: 2
height: {
if (MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing && CavaService.values.length > index) {
const rawLevel = CavaService.values[index] || 0
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100
const maxHeight = Theme.iconSize - 2
const minHeight = 3
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight)
}
return 3
}
radius: 1.5
color: "white"
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
}
}
}
StyledText {
text: {
const player = MprisController.activePlayer
if (!player?.trackTitle)
return ""
const title = player.trackTitle
const artist = player.trackArtist || ""
return artist ? title + " • " + artist : title
}
font.pixelSize: Theme.fontSizeLarge
color: "white"
opacity: 0.9
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: Math.min(implicitWidth, 400)
wrapMode: Text.NoWrap
maximumLineCount: 1
}
Row {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
color: prevArea.containsMouse ? Qt.rgba(255, 255, 255, 0.2) : "transparent"
visible: MprisController.activePlayer
opacity: (MprisController.activePlayer?.canGoPrevious ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 12
color: "white"
}
MouseArea {
id: prevArea
anchors.fill: parent
enabled: MprisController.activePlayer?.canGoPrevious ?? false
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: MprisController.activePlayer?.previous()
}
}
Rectangle {
width: 24
height: 24
radius: 12
anchors.verticalCenter: parent.verticalCenter
color: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing ? Qt.rgba(255, 255, 255, 0.9) : Qt.rgba(255, 255, 255, 0.2)
visible: MprisController.activePlayer
DankIcon {
anchors.centerIn: parent
name: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 14
color: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing ? "black" : "white"
}
MouseArea {
anchors.fill: parent
enabled: MprisController.activePlayer
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: MprisController.activePlayer?.togglePlaying()
}
}
Rectangle {
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
color: nextArea.containsMouse ? Qt.rgba(255, 255, 255, 0.2) : "transparent"
visible: MprisController.activePlayer
opacity: (MprisController.activePlayer?.canGoNext ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 12
color: "white"
}
MouseArea {
id: nextArea
anchors.fill: parent
enabled: MprisController.activePlayer?.canGoNext ?? false
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: MprisController.activePlayer?.next()
}
}
}
}
Rectangle {
width: 1
height: 24
color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter
visible: MprisController.activePlayer && WeatherService.weather.available
}
Row {
@@ -1060,6 +1247,7 @@ Item {
}
function onReadyToLaunch() {
root.sessionLock.locked = false
GreeterState.unlocking = true
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]
if (sessionCmd) {

View File

@@ -1,34 +1,18 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Greetd
import qs.Common
Variants {
model: Quickshell.screens
WlSessionLockSurface {
id: root
PanelWindow {
id: root
required property WlSessionLock lock
property var modelData
color: "transparent"
screen: modelData
anchors {
left: true
right: true
top: true
bottom: true
}
exclusionMode: ExclusionMode.Normal
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
GreeterContent {
anchors.fill: parent
screenName: root.screen?.name ?? ""
}
GreeterContent {
anchors.fill: parent
screenName: root.screen?.name ?? ""
sessionLock: root.lock
}
}

View File

@@ -1,284 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
Scope {
id: overviewScope
property bool overviewOpen: false
Loader {
id: hyprlandLoader
active: overviewScope.overviewOpen
asynchronous: false
sourceComponent: Variants {
id: overviewVariants
model: Quickshell.screens
PanelWindow {
id: root
required property var modelData
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
screen: modelData
visible: overviewScope.overviewOpen
color: "transparent"
WlrLayershell.namespace: "quickshell:overview"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
anchors {
top: true
left: true
right: true
bottom: true
}
HyprlandFocusGrab {
id: grab
windows: [root]
active: false
property bool hasBeenActivated: false
onActiveChanged: {
if (active) {
hasBeenActivated = true
}
}
onCleared: () => {
if (hasBeenActivated && overviewScope.overviewOpen) {
overviewScope.overviewOpen = false
}
}
}
Connections {
target: overviewScope
function onOverviewOpenChanged() {
if (overviewScope.overviewOpen) {
grab.hasBeenActivated = false
delayedGrabTimer.start()
} else {
delayedGrabTimer.stop()
grab.active = false
grab.hasBeenActivated = false
}
}
}
Connections {
target: root
function onMonitorIsFocusedChanged() {
if (overviewScope.overviewOpen && root.monitorIsFocused && !grab.active) {
grab.hasBeenActivated = false
grab.active = true
} else if (overviewScope.overviewOpen && !root.monitorIsFocused && grab.active) {
grab.active = false
}
}
}
Timer {
id: delayedGrabTimer
interval: 150
repeat: false
onTriggered: {
if (overviewScope.overviewOpen && root.monitorIsFocused) {
grab.active = true
}
}
}
Timer {
id: closeTimer
interval: Theme.expressiveDurations.expressiveDefaultSpatial + 120
onTriggered: {
root.visible = false
}
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: overviewScope.overviewOpen ? 0.5 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
MouseArea {
anchors.fill: parent
onClicked: mouse => {
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
overviewScope.overviewOpen = false
closeTimer.restart()
}
}
}
}
Item {
id: contentContainer
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 100
width: childrenRect.width
height: childrenRect.height
opacity: overviewScope.overviewOpen ? 1 : 0
transform: [scaleTransform, motionTransform]
Scale {
id: scaleTransform
origin.x: contentContainer.width / 2
origin.y: contentContainer.height / 2
xScale: overviewScope.overviewOpen ? 1 : 0.96
yScale: overviewScope.overviewOpen ? 1 : 0.96
Behavior on xScale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on yScale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
}
Translate {
id: motionTransform
x: 0
y: overviewScope.overviewOpen ? 0 : Theme.spacingL
Behavior on y {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Loader {
id: overviewLoader
active: overviewScope.overviewOpen
asynchronous: false
sourceComponent: OverviewWidget {
panelWindow: root
overviewOpen: overviewScope.overviewOpen
}
}
}
FocusScope {
id: focusScope
anchors.fill: parent
visible: overviewScope.overviewOpen
focus: overviewScope.overviewOpen && root.monitorIsFocused
Keys.onEscapePressed: event => {
if (!root.monitorIsFocused) return
overviewScope.overviewOpen = false
closeTimer.restart()
event.accepted = true
}
Keys.onPressed: event => {
if (!root.monitorIsFocused) return
if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) {
if (!overviewLoader.item) return
const thisMonitorWorkspaceIds = overviewLoader.item.thisMonitorWorkspaceIds
if (thisMonitorWorkspaceIds.length === 0) return
const currentId = root.monitor.activeWorkspace?.id ?? thisMonitorWorkspaceIds[0]
const currentIndex = thisMonitorWorkspaceIds.indexOf(currentId)
let targetIndex
if (event.key === Qt.Key_Left) {
targetIndex = currentIndex - 1
if (targetIndex < 0) targetIndex = thisMonitorWorkspaceIds.length - 1
} else {
targetIndex = currentIndex + 1
if (targetIndex >= thisMonitorWorkspaceIds.length) targetIndex = 0
}
const targetId = thisMonitorWorkspaceIds[targetIndex]
Hyprland.dispatch("workspace " + targetId)
event.accepted = true
}
}
onVisibleChanged: {
if (visible && overviewScope.overviewOpen && root.monitorIsFocused) {
Qt.callLater(() => focusScope.forceActiveFocus())
}
}
Connections {
target: root
function onMonitorIsFocusedChanged() {
if (root.monitorIsFocused && overviewScope.overviewOpen) {
Qt.callLater(() => focusScope.forceActiveFocus())
}
}
}
}
onVisibleChanged: {
if (visible && overviewScope.overviewOpen) {
Qt.callLater(() => focusScope.forceActiveFocus())
} else if (!visible) {
grab.active = false
}
}
Connections {
target: overviewScope
function onOverviewOpenChanged() {
if (overviewScope.overviewOpen) {
closeTimer.stop()
root.visible = true
Qt.callLater(() => focusScope.forceActiveFocus())
} else {
closeTimer.restart()
grab.active = false
}
}
}
}
}
}
}

View File

@@ -1,428 +0,0 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
required property var panelWindow
required property bool overviewOpen
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
readonly property int workspacesShown: SettingsData.overviewRows * SettingsData.overviewColumns
readonly property var allWorkspaces: Hyprland.workspaces?.values || []
readonly property var allWorkspaceIds: {
const workspaces = allWorkspaces
if (!workspaces || workspaces.length === 0) return []
try {
const ids = workspaces.map(ws => ws?.id).filter(id => id !== null && id !== undefined)
return ids.sort((a, b) => a - b)
} catch (e) {
return []
}
}
readonly property var thisMonitorWorkspaceIds: {
const workspaces = allWorkspaces
const mon = monitor
if (!workspaces || workspaces.length === 0 || !mon) return []
try {
const filtered = workspaces.filter(ws => ws?.monitor?.name === mon.name)
return filtered.map(ws => ws?.id).filter(id => id !== null && id !== undefined).sort((a, b) => a - b)
} catch (e) {
return []
}
}
readonly property var displayedWorkspaceIds: {
if (!allWorkspaceIds || allWorkspaceIds.length === 0) {
const result = []
for (let i = 1; i <= workspacesShown; i++) {
result.push(i)
}
return result
}
try {
const maxExisting = Math.max(...allWorkspaceIds)
const totalNeeded = Math.max(workspacesShown, allWorkspaceIds.length)
const result = []
for (let i = 1; i <= maxExisting; i++) {
result.push(i)
}
let nextId = maxExisting + 1
while (result.length < totalNeeded) {
result.push(nextId)
nextId++
}
return result
} catch (e) {
const result = []
for (let i = 1; i <= workspacesShown; i++) {
result.push(i)
}
return result
}
}
readonly property int minWorkspaceId: displayedWorkspaceIds.length > 0 ? displayedWorkspaceIds[0] : 1
readonly property int maxWorkspaceId: displayedWorkspaceIds.length > 0 ? displayedWorkspaceIds[displayedWorkspaceIds.length - 1] : workspacesShown
readonly property int displayWorkspaceCount: displayedWorkspaceIds.length
function getWorkspaceMonitorName(workspaceId) {
if (!allWorkspaces || !workspaceId) return ""
try {
const ws = allWorkspaces.find(w => w?.id === workspaceId)
return ws?.monitor?.name ?? ""
} catch (e) {
return ""
}
}
function workspaceHasWindows(workspaceId) {
if (!workspaceId) return false
try {
const workspace = allWorkspaces.find(ws => ws?.id === workspaceId)
if (!workspace) return false
const toplevels = workspace?.toplevels?.values || []
return toplevels.length > 0
} catch (e) {
return false
}
}
property bool monitorIsFocused: monitor?.focused ?? false
property real scale: SettingsData.overviewScale
property color activeBorderColor: Theme.primary
property real workspaceImplicitWidth: ((monitor.width / monitor.scale) * root.scale)
property real workspaceImplicitHeight: ((monitor.height / monitor.scale) * root.scale)
property int workspaceZ: 0
property int windowZ: 1
property int monitorLabelZ: 2
property int windowDraggingZ: 99999
property real workspaceSpacing: 5
property int draggingFromWorkspace: -1
property int draggingTargetWorkspace: -1
implicitWidth: overviewBackground.implicitWidth + Theme.spacingL * 2
implicitHeight: overviewBackground.implicitHeight + Theme.spacingL * 2
Component.onCompleted: {
Hyprland.refreshToplevels()
Hyprland.refreshWorkspaces()
Hyprland.refreshMonitors()
}
onOverviewOpenChanged: {
if (overviewOpen) {
Hyprland.refreshToplevels()
Hyprland.refreshWorkspaces()
Hyprland.refreshMonitors()
}
}
Rectangle {
id: overviewBackground
property real padding: 10
anchors.fill: parent
anchors.margins: Theme.spacingL
implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2
implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowBlur: 0.5
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowColor: Theme.shadowStrong
shadowOpacity: 1
blurMax: 32
}
ColumnLayout {
id: workspaceColumnLayout
z: root.workspaceZ
anchors.centerIn: parent
spacing: workspaceSpacing
Repeater {
model: SettingsData.overviewRows
delegate: RowLayout {
id: row
property int rowIndex: index
spacing: workspaceSpacing
Repeater {
model: SettingsData.overviewColumns
Rectangle {
id: workspace
property int colIndex: index
property int workspaceIndex: rowIndex * SettingsData.overviewColumns + colIndex
property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null
property bool isActive: workspaceObj?.active ?? false
property bool isOnThisMonitor: (workspaceObj && root.monitor) ? (workspaceObj.monitor?.name === root.monitor.name) : true
property bool hasWindows: (workspaceValue > 0) ? root.workspaceHasWindows(workspaceValue) : false
property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : ""
property color defaultWorkspaceColor: workspaceExists ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.3)
property color hoveredWorkspaceColor: Qt.lighter(defaultWorkspaceColor, 1.1)
property color hoveredBorderColor: Theme.surfaceVariant
property bool hoveredWhileDragging: false
property bool shouldShowActiveIndicator: isActive && isOnThisMonitor && hasWindows
visible: workspaceValue !== -1
implicitWidth: root.workspaceImplicitWidth
implicitHeight: root.workspaceImplicitHeight
color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
radius: Theme.cornerRadius
border.width: 2
border.color: hoveredWhileDragging ? hoveredBorderColor : (shouldShowActiveIndicator ? root.activeBorderColor : "transparent")
StyledText {
anchors.centerIn: parent
text: workspaceValue
font.pixelSize: Theme.fontSizeXLarge * 6
font.weight: Font.DemiBold
color: Theme.withAlpha(Theme.surfaceText, workspaceExists ? 0.2 : 0.1)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
MouseArea {
id: workspaceArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
if (root.draggingTargetWorkspace === -1) {
root.overviewOpen = false
Hyprland.dispatch(`workspace ${workspaceValue}`)
}
}
}
DropArea {
anchors.fill: parent
onEntered: {
root.draggingTargetWorkspace = workspaceValue
if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return
hoveredWhileDragging = true
}
onExited: {
hoveredWhileDragging = false
if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1
}
}
}
}
}
}
}
Item {
id: windowSpace
anchors.centerIn: parent
implicitWidth: workspaceColumnLayout.implicitWidth
implicitHeight: workspaceColumnLayout.implicitHeight
Repeater {
model: ScriptModel {
values: {
const workspaces = root.allWorkspaces
const minId = root.minWorkspaceId
const maxId = root.maxWorkspaceId
if (!workspaces || workspaces.length === 0) return []
try {
const result = []
for (const workspace of workspaces) {
const wsId = workspace?.id ?? -1
if (wsId >= minId && wsId <= maxId) {
const toplevels = workspace?.toplevels?.values || []
for (const toplevel of toplevels) {
result.push(toplevel)
}
}
}
return result
} catch (e) {
console.error("OverviewWidget filter error:", e)
return []
}
}
}
delegate: OverviewWindow {
id: window
required property var modelData
overviewOpen: root.overviewOpen
readonly property int windowWorkspaceId: modelData?.workspace?.id ?? -1
function getWorkspaceIndex() {
if (!root.displayedWorkspaceIds || root.displayedWorkspaceIds.length === 0) return 0
if (!windowWorkspaceId || windowWorkspaceId < 0) return 0
try {
for (let i = 0; i < root.displayedWorkspaceIds.length; i++) {
if (root.displayedWorkspaceIds[i] === windowWorkspaceId) {
return i
}
}
return 0
} catch (e) {
return 0
}
}
readonly property int workspaceIndex: getWorkspaceIndex()
readonly property int workspaceColIndex: workspaceIndex % SettingsData.overviewColumns
readonly property int workspaceRowIndex: Math.floor(workspaceIndex / SettingsData.overviewColumns)
toplevel: modelData
scale: root.scale
availableWorkspaceWidth: root.workspaceImplicitWidth
availableWorkspaceHeight: root.workspaceImplicitHeight
widgetMonitorId: root.monitor.id
xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex
yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex
z: atInitPosition ? root.windowZ : root.windowDraggingZ
property bool atInitPosition: (initX == x && initY == y)
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
MouseArea {
id: dragArea
anchors.fill: parent
hoverEnabled: true
onEntered: window.hovered = true
onExited: window.hovered = false
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
drag.target: parent
onPressed: (mouse) => {
root.draggingFromWorkspace = windowData?.workspace.id
window.pressed = true
window.Drag.active = true
window.Drag.source = window
window.Drag.hotSpot.x = mouse.x
window.Drag.hotSpot.y = mouse.y
}
onReleased: {
const targetWorkspace = root.draggingTargetWorkspace
window.pressed = false
window.Drag.active = false
root.draggingFromWorkspace = -1
root.draggingTargetWorkspace = -1
if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) {
Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace},address:${windowData?.address}`)
Qt.callLater(() => {
Hyprland.refreshToplevels()
Hyprland.refreshWorkspaces()
Qt.callLater(() => {
window.x = window.initX
window.y = window.initY
})
})
} else {
window.x = window.initX
window.y = window.initY
}
}
onClicked: (event) => {
if (!windowData) return
if (event.button === Qt.LeftButton) {
root.overviewOpen = false
Hyprland.dispatch(`focuswindow address:${windowData.address}`)
event.accepted = true
} else if (event.button === Qt.MiddleButton) {
Hyprland.dispatch(`closewindow address:${windowData.address}`)
event.accepted = true
}
}
}
}
}
}
Item {
id: monitorLabelSpace
anchors.centerIn: parent
implicitWidth: workspaceColumnLayout.implicitWidth
implicitHeight: workspaceColumnLayout.implicitHeight
z: root.monitorLabelZ
Repeater {
model: SettingsData.overviewRows
delegate: Item {
id: labelRow
property int rowIndex: index
y: (root.workspaceImplicitHeight + workspaceSpacing) * rowIndex
width: parent.width
height: root.workspaceImplicitHeight
Repeater {
model: SettingsData.overviewColumns
delegate: Item {
id: labelItem
property int colIndex: index
property int workspaceIndex: labelRow.rowIndex * SettingsData.overviewColumns + colIndex
property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : ""
x: (root.workspaceImplicitWidth + workspaceSpacing) * colIndex
width: root.workspaceImplicitWidth
height: root.workspaceImplicitHeight
Rectangle {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingS
width: monitorNameText.contentWidth + Theme.spacingS * 2
height: monitorNameText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius
color: Theme.surface
visible: labelItem.workspaceExists && labelItem.workspaceMonitorName !== ""
StyledText {
id: monitorNameText
anchors.centerIn: parent
text: labelItem.workspaceMonitorName
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
}
}
}
}

View File

@@ -1,140 +0,0 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Common
Item {
id: root
property var toplevel
property var scale
required property bool overviewOpen
property var availableWorkspaceWidth
property var availableWorkspaceHeight
property bool restrictToWorkspace: true
readonly property var windowData: toplevel?.lastIpcObject || null
readonly property var monitorObj: toplevel?.monitor
readonly property var monitorData: monitorObj?.lastIpcObject || null
property real initX: Math.max(((windowData?.at?.[0] ?? 0) - (monitorData?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * root.scale, 0) + xOffset
property real initY: Math.max(((windowData?.at?.[1] ?? 0) - (monitorData?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * root.scale, 0) + yOffset
property real xOffset: 0
property real yOffset: 0
property int widgetMonitorId: 0
property var targetWindowWidth: (windowData?.size?.[0] ?? 100) * scale
property var targetWindowHeight: (windowData?.size?.[1] ?? 100) * scale
property bool hovered: false
property bool pressed: false
property var iconToWindowRatio: 0.25
property var iconToWindowRatioCompact: 0.45
property var entry: DesktopEntries.heuristicLookup(windowData?.class)
property var iconPath: Quickshell.iconPath(entry?.icon ?? windowData?.class ?? "application-x-executable", "image-missing")
property bool compactMode: Theme.fontSizeSmall * 4 > targetWindowHeight || Theme.fontSizeSmall * 4 > targetWindowWidth
x: initX
y: initY
width: Math.min((windowData?.size?.[0] ?? 100) * root.scale, availableWorkspaceWidth)
height: Math.min((windowData?.size?.[1] ?? 100) * root.scale, availableWorkspaceHeight)
opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4
Rectangle {
id: maskRect
width: root.width
height: root.height
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: maskRect
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Behavior on x {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
Behavior on y {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
Behavior on width {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
Behavior on height {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
ScreencopyView {
id: windowPreview
anchors.fill: parent
captureSource: root.overviewOpen ? root.toplevel?.wayland : null
live: true
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) :
hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) :
Theme.withAlpha(Theme.surfaceContainer, 0.1)
border.color: Theme.withAlpha(Theme.outline, 0.3)
border.width: 1
}
ColumnLayout {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.fontSizeSmall * 0.5
Image {
id: windowIcon
property var iconSize: {
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1)
}
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
Behavior on width {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
Behavior on height {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
}
}
}
}

View File

@@ -537,7 +537,7 @@ Rectangle {
StyledText {
id: clearText
text: I18n.tr("Dismiss")
text: I18n.tr("Clear")
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
@@ -630,7 +630,7 @@ Rectangle {
StyledText {
id: clearText
text: I18n.tr("Dismiss")
text: I18n.tr("Clear")
color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium

View File

@@ -99,7 +99,7 @@ Item {
}
StyledText {
text: I18n.tr("Clear")
text: I18n.tr("Clear All")
font.pixelSize: Theme.fontSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium

View File

@@ -21,7 +21,7 @@ PanelWindow {
property bool exiting: false
property bool _isDestroying: false
property bool _finalized: false
readonly property string clearText: I18n.tr("Dismiss")
readonly property string clearText: I18n.tr("Clear")
signal entered
signal exitFinished
@@ -512,7 +512,7 @@ PanelWindow {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
acceptedButtons: Qt.LeftButton
propagateComposedEvents: true
z: -1
onEntered: {
@@ -523,20 +523,9 @@ PanelWindow {
if (notificationData && notificationData.popup && notificationData.timer)
notificationData.timer.restart()
}
onClicked: (mouse) => {
if (!notificationData || win.exiting)
return
if (mouse.button === Qt.RightButton) {
NotificationService.dismissNotification(notificationData)
} else if (mouse.button === Qt.LeftButton) {
if (notificationData.actions && notificationData.actions.length > 0) {
notificationData.actions[0].invoke()
NotificationService.dismissNotification(notificationData)
} else {
notificationData.popup = false
}
}
onClicked: {
if (notificationData && !win.exiting)
notificationData.popup = false
}
}
}