mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
dock: create an initial basic dock
This commit is contained in:
188
Modules/Dock/Dock.qml
Normal file
188
Modules/Dock/Dock.qml
Normal file
@@ -0,0 +1,188 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: dock
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
property var modelData
|
||||
property var contextMenu
|
||||
property var windowsMenu
|
||||
property bool autoHide: Prefs.dockAutoHide
|
||||
property real backgroundTransparency: Prefs.dockTransparency
|
||||
|
||||
|
||||
property bool contextMenuOpen: (contextMenu && contextMenu.visible && contextMenu.screen === modelData) || (windowsMenu && windowsMenu.visible && windowsMenu.screen === modelData)
|
||||
property bool windowIsFullscreen: {
|
||||
if (!NiriService.focusedWindowId || !NiriService.niriAvailable) return false
|
||||
var focusedWindow = NiriService.windows.find(w => w.id === NiriService.focusedWindowId)
|
||||
if (!focusedWindow) return false
|
||||
var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
|
||||
return fullscreenApps.some(app => focusedWindow.app_id && focusedWindow.app_id.toLowerCase().includes(app))
|
||||
}
|
||||
property bool reveal: (!autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen) && !windowIsFullscreen
|
||||
|
||||
Connections {
|
||||
target: Prefs
|
||||
function onDockTransparencyChanged() {
|
||||
dock.backgroundTransparency = Prefs.dockTransparency;
|
||||
}
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
visible: Prefs.showDock
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
margins {
|
||||
left: 0
|
||||
right: 0
|
||||
}
|
||||
|
||||
implicitHeight: 100
|
||||
exclusiveZone: autoHide ? -1 : 65 - 16
|
||||
|
||||
mask: Region {
|
||||
item: dockMouseArea
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dockMouseArea
|
||||
height: dock.reveal ? 65 : 12
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
implicitWidth: dock.reveal ? dockBackground.width + 32 : (dockBackground.width + 32)
|
||||
hoverEnabled: true
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
id: dockContainer
|
||||
anchors.fill: parent
|
||||
|
||||
transform: Translate {
|
||||
id: dockSlide
|
||||
y: dock.reveal ? 0 : 60
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dockBackground
|
||||
objectName: "dockBackground"
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
width: dockApps.implicitWidth + 12
|
||||
height: parent.height - 8
|
||||
|
||||
anchors.topMargin: 4
|
||||
anchors.bottomMargin: 1
|
||||
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
|
||||
radius: Theme.cornerRadiusXLarge
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
layer.enabled: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
radius: parent.radius
|
||||
}
|
||||
|
||||
DockApps {
|
||||
id: dockApps
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: 4
|
||||
anchors.bottomMargin: 4
|
||||
|
||||
contextMenu: dock.contextMenu
|
||||
windowsMenu: dock.windowsMenu
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: appTooltip
|
||||
|
||||
property var hoveredButton: {
|
||||
if (!dockApps.children[0]) return null
|
||||
var row = dockApps.children[0]
|
||||
var repeater = null
|
||||
for (var i = 0; i < row.children.length; i++) {
|
||||
var child = row.children[i]
|
||||
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
|
||||
repeater = child
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!repeater || !repeater.itemAt) return null
|
||||
for (var i = 0; i < repeater.count; i++) {
|
||||
var item = repeater.itemAt(i)
|
||||
if (item && item.dockButton && item.dockButton.showTooltip) {
|
||||
return item.dockButton
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
|
||||
|
||||
visible: hoveredButton !== null && tooltipText !== ""
|
||||
width: tooltipLabel.implicitWidth + 24
|
||||
height: tooltipLabel.implicitHeight + 12
|
||||
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
|
||||
y: -height - 8
|
||||
x: hoveredButton ? hoveredButton.mapToItem(dockContainer, hoveredButton.width/2, 0).x - width/2 : 0
|
||||
|
||||
StyledText {
|
||||
id: tooltipLabel
|
||||
anchors.centerIn: parent
|
||||
text: appTooltip.tooltipText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
289
Modules/Dock/DockAppButton.qml
Normal file
289
Modules/Dock/DockAppButton.qml
Normal file
@@ -0,0 +1,289 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var appData
|
||||
property var contextMenu: null
|
||||
property var windowsMenu: null
|
||||
property var dockApps: null
|
||||
property int index: -1
|
||||
property bool longPressing: false
|
||||
property bool dragging: false
|
||||
property point dragStartPos: Qt.point(0, 0)
|
||||
property point dragOffset: Qt.point(0, 0)
|
||||
property int targetIndex: -1
|
||||
property int originalIndex: -1
|
||||
|
||||
width: 40
|
||||
height: 40
|
||||
|
||||
property bool isHovered: mouseArea.containsMouse && !dragging
|
||||
|
||||
transform: Translate {
|
||||
id: translateY
|
||||
y: 0
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: bounceAnimation
|
||||
running: false
|
||||
|
||||
NumberAnimation {
|
||||
target: translateY
|
||||
property: "y"
|
||||
to: -10
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasizedAccel
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: translateY
|
||||
property: "y"
|
||||
to: -8
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: exitAnimation
|
||||
running: false
|
||||
target: translateY
|
||||
property: "y"
|
||||
to: 0
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasizedDecel
|
||||
}
|
||||
|
||||
onIsHoveredChanged: {
|
||||
if (isHovered) {
|
||||
exitAnimation.stop()
|
||||
if (!bounceAnimation.running) {
|
||||
bounceAnimation.restart()
|
||||
}
|
||||
} else {
|
||||
bounceAnimation.stop()
|
||||
exitAnimation.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 2
|
||||
border.color: Theme.primary
|
||||
visible: dragging
|
||||
z: -1
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: longPressTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (appData && appData.isPinned) {
|
||||
longPressing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: -20
|
||||
hoverEnabled: true
|
||||
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
|
||||
|
||||
onPressed: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton && appData && appData.isPinned) {
|
||||
dragStartPos = Qt.point(mouse.x, mouse.y)
|
||||
longPressTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: (mouse) => {
|
||||
longPressTimer.stop()
|
||||
if (longPressing) {
|
||||
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps) {
|
||||
dockApps.movePinnedApp(originalIndex, targetIndex)
|
||||
}
|
||||
|
||||
longPressing = false
|
||||
dragging = false
|
||||
dragOffset = Qt.point(0, 0)
|
||||
targetIndex = -1
|
||||
originalIndex = -1
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: (mouse) => {
|
||||
if (longPressing && !dragging) {
|
||||
var distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2))
|
||||
if (distance > 5) {
|
||||
dragging = true
|
||||
targetIndex = index
|
||||
originalIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y)
|
||||
|
||||
if (dockApps) {
|
||||
var threshold = 40
|
||||
var newTargetIndex = targetIndex
|
||||
|
||||
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) {
|
||||
newTargetIndex = targetIndex + 1
|
||||
} else if (dragOffset.x < -threshold && targetIndex > 0) {
|
||||
newTargetIndex = targetIndex - 1
|
||||
}
|
||||
|
||||
if (newTargetIndex !== targetIndex) {
|
||||
targetIndex = newTargetIndex
|
||||
dragStartPos = Qt.point(mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (!appData || longPressing) return
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
var windowCount = appData.windows ? appData.windows.count : 0
|
||||
|
||||
if (windowCount === 0) {
|
||||
if (appData && appData.appId) {
|
||||
var desktopEntry = DesktopEntries.byId(appData.appId)
|
||||
if (desktopEntry) {
|
||||
Prefs.addAppUsage({
|
||||
id: appData.appId,
|
||||
name: desktopEntry.name || appData.appId,
|
||||
icon: desktopEntry.icon || "",
|
||||
exec: desktopEntry.exec || "",
|
||||
comment: desktopEntry.comment || ""
|
||||
})
|
||||
}
|
||||
Quickshell.execDetached(["gtk-launch", appData.appId])
|
||||
}
|
||||
} else if (windowCount === 1) {
|
||||
var window = appData.windows.get(0)
|
||||
NiriService.focusWindow(window.id)
|
||||
} else {
|
||||
windowsMenu.showForButton(root, appData, 40)
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
if (appData && appData.appId) {
|
||||
var desktopEntry = DesktopEntries.byId(appData.appId)
|
||||
if (desktopEntry) {
|
||||
Prefs.addAppUsage({
|
||||
id: appData.appId,
|
||||
name: desktopEntry.name || appData.appId,
|
||||
icon: desktopEntry.icon || "",
|
||||
exec: desktopEntry.exec || "",
|
||||
comment: desktopEntry.comment || ""
|
||||
})
|
||||
}
|
||||
Quickshell.execDetached(["gtk-launch", appData.appId])
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (contextMenu) {
|
||||
contextMenu.showForButton(root, appData, 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool showTooltip: mouseArea.containsMouse && !dragging
|
||||
property string tooltipText: {
|
||||
if (!appData || !appData.appId) return ""
|
||||
var desktopEntry = DesktopEntries.byId(appData.appId)
|
||||
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId
|
||||
}
|
||||
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
width: 40
|
||||
height: 40
|
||||
anchors.centerIn: parent
|
||||
source: {
|
||||
if (!appData || !appData.appId) return ""
|
||||
var desktopEntry = DesktopEntries.byId(appData.appId)
|
||||
if (desktopEntry && desktopEntry.icon) {
|
||||
var iconPath = Quickshell.iconPath(desktopEntry.icon, Prefs.iconTheme === "System Default" ? "" : Prefs.iconTheme)
|
||||
return iconPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
implicitSize: 40
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
anchors.centerIn: parent
|
||||
visible: !iconImg.visible
|
||||
color: Theme.surfaceLight
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (!appData || !appData.appId) return "?"
|
||||
var desktopEntry = DesktopEntries.byId(appData.appId)
|
||||
if (desktopEntry && desktopEntry.name) {
|
||||
return desktopEntry.name.charAt(0).toUpperCase()
|
||||
}
|
||||
return appData.appId.charAt(0).toUpperCase()
|
||||
}
|
||||
font.pixelSize: 14
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: -2
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: appData && appData.windows ? Math.min(appData.windows.count, 4) : 0
|
||||
|
||||
Rectangle {
|
||||
width: appData && appData.windows && appData.windows.count <= 3 ? 5 : 3
|
||||
height: 2
|
||||
radius: 1
|
||||
color: {
|
||||
if (!appData || !appData.windows || appData.windows.count === 0) return "transparent"
|
||||
var window = appData.windows.get(index)
|
||||
return window && window.id == NiriService.focusedWindowId ? Theme.primary :
|
||||
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
164
Modules/Dock/DockApps.qml
Normal file
164
Modules/Dock/DockApps.qml
Normal file
@@ -0,0 +1,164 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var contextMenu: null
|
||||
property var windowsMenu: null
|
||||
property bool requestDockShow: false
|
||||
property int pinnedAppCount: 0
|
||||
|
||||
implicitWidth: row.width
|
||||
implicitHeight: row.height
|
||||
|
||||
function movePinnedApp(fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex) return
|
||||
|
||||
var currentPinned = [...Prefs.pinnedApps]
|
||||
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) return
|
||||
|
||||
var movedApp = currentPinned.splice(fromIndex, 1)[0]
|
||||
currentPinned.splice(toIndex, 0, movedApp)
|
||||
|
||||
Prefs.setPinnedApps(currentPinned)
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
spacing: 2
|
||||
anchors.centerIn: parent
|
||||
height: 40
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: ListModel {
|
||||
id: dockModel
|
||||
|
||||
Component.onCompleted: updateModel()
|
||||
|
||||
function updateModel() {
|
||||
clear()
|
||||
|
||||
var items = []
|
||||
var runningApps = NiriService.getRunningAppIds()
|
||||
var pinnedApps = [...Prefs.pinnedApps]
|
||||
var addedApps = new Set()
|
||||
|
||||
pinnedApps.forEach(appId => {
|
||||
var lowerAppId = appId.toLowerCase()
|
||||
if (!addedApps.has(lowerAppId)) {
|
||||
var windows = NiriService.getWindowsByAppId(appId)
|
||||
items.push({
|
||||
appId: appId,
|
||||
windows: windows,
|
||||
isPinned: true,
|
||||
isRunning: windows.length > 0
|
||||
})
|
||||
addedApps.add(lowerAppId)
|
||||
}
|
||||
})
|
||||
root.pinnedAppCount = pinnedApps.length
|
||||
var unpinnedApps = []
|
||||
runningApps.forEach(appId => {
|
||||
var lowerAppId = appId.toLowerCase()
|
||||
if (!addedApps.has(lowerAppId)) {
|
||||
unpinnedApps.push(appId)
|
||||
addedApps.add(lowerAppId)
|
||||
}
|
||||
})
|
||||
var appUsageRanking = Prefs.appUsageRanking || {}
|
||||
var rankedApps = []
|
||||
|
||||
for (var appId in appUsageRanking) {
|
||||
var lowerAppId = appId.toLowerCase()
|
||||
if (!addedApps.has(lowerAppId)) {
|
||||
rankedApps.push({
|
||||
appId: appId,
|
||||
lastUsed: appUsageRanking[appId].lastUsed || 0,
|
||||
usageCount: appUsageRanking[appId].usageCount || 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
rankedApps.sort((a, b) => b.lastUsed - a.lastUsed)
|
||||
|
||||
var recentToAdd = Math.min(3, rankedApps.length)
|
||||
for (var i = 0; i < recentToAdd; i++) {
|
||||
var appId = rankedApps[i].appId
|
||||
var lowerAppId = appId.toLowerCase()
|
||||
unpinnedApps.push(appId)
|
||||
addedApps.add(lowerAppId)
|
||||
}
|
||||
if (pinnedApps.length > 0 && unpinnedApps.length > 0) {
|
||||
items.push({
|
||||
appId: "__SEPARATOR__",
|
||||
windows: [],
|
||||
isPinned: false,
|
||||
isRunning: false
|
||||
})
|
||||
}
|
||||
unpinnedApps.forEach(appId => {
|
||||
var windows = NiriService.getWindowsByAppId(appId)
|
||||
items.push({
|
||||
appId: appId,
|
||||
windows: windows,
|
||||
isPinned: false,
|
||||
isRunning: windows.length > 0
|
||||
})
|
||||
})
|
||||
items.forEach(item => {
|
||||
append(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
property alias dockButton: button
|
||||
|
||||
width: model.appId === "__SEPARATOR__" ? 16 : 40
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
visible: model.appId === "__SEPARATOR__"
|
||||
width: 2
|
||||
height: 20
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
radius: 1
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
DockAppButton {
|
||||
id: button
|
||||
visible: model.appId !== "__SEPARATOR__"
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: 40
|
||||
height: 40
|
||||
|
||||
appData: model
|
||||
contextMenu: root.contextMenu
|
||||
windowsMenu: root.windowsMenu
|
||||
dockApps: root
|
||||
index: model.index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NiriService
|
||||
function onWindowsChanged() { dockModel.updateModel() }
|
||||
function onWindowOpenedOrChanged() { dockModel.updateModel() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Prefs
|
||||
function onPinnedAppsChanged() { dockModel.updateModel() }
|
||||
}
|
||||
}
|
||||
283
Modules/Dock/DockContextMenu.qml
Normal file
283
Modules/Dock/DockContextMenu.qml
Normal file
@@ -0,0 +1,283 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool showContextMenu: false
|
||||
property var appData: null
|
||||
property var anchorItem: null
|
||||
property real dockVisibleHeight: 40
|
||||
property int margin: 10
|
||||
|
||||
function showForButton(button, data, dockHeight) {
|
||||
anchorItem = button
|
||||
appData = data
|
||||
dockVisibleHeight = dockHeight || 40
|
||||
|
||||
var dockWindow = button.Window.window
|
||||
if (dockWindow) {
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
var s = Quickshell.screens[i]
|
||||
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
|
||||
root.screen = s
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showContextMenu = true
|
||||
}
|
||||
function close() { showContextMenu = false }
|
||||
|
||||
screen: Quickshell.screens[0]
|
||||
|
||||
visible: showContextMenu
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
anchors { top: true; left: true; right: true; bottom: true }
|
||||
|
||||
property point anchorPos: Qt.point(screen.width/2, screen.height - 100)
|
||||
|
||||
onAnchorItemChanged: updatePosition()
|
||||
onVisibleChanged: if (visible) updatePosition()
|
||||
|
||||
function updatePosition() {
|
||||
if (!anchorItem) {
|
||||
anchorPos = Qt.point(screen.width/2, screen.height - 100)
|
||||
return
|
||||
}
|
||||
|
||||
var dockWindow = anchorItem.Window.window
|
||||
if (!dockWindow) {
|
||||
anchorPos = Qt.point(screen.width/2, screen.height - 100)
|
||||
return
|
||||
}
|
||||
|
||||
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
|
||||
|
||||
var actualDockHeight = root.dockVisibleHeight // fallback
|
||||
|
||||
function findDockBackground(item) {
|
||||
if (item.objectName === "dockBackground") {
|
||||
return item
|
||||
}
|
||||
for (var i = 0; i < item.children.length; i++) {
|
||||
var found = findDockBackground(item.children[i])
|
||||
if (found) return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
var dockBackground = findDockBackground(dockWindow.contentItem)
|
||||
if (dockBackground) {
|
||||
actualDockHeight = dockBackground.height
|
||||
}
|
||||
|
||||
var dockBottomMargin = 16 // The dock has bottom margin
|
||||
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
|
||||
|
||||
var dockContentWidth = dockWindow.width
|
||||
var screenWidth = root.screen.width
|
||||
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
|
||||
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width/2
|
||||
|
||||
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuContainer
|
||||
|
||||
width: Math.min(400, Math.max(200, menuColumn.implicitWidth + Theme.spacingS * 2))
|
||||
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
|
||||
|
||||
x: {
|
||||
var left = 10
|
||||
var right = root.width - width - 10
|
||||
var want = root.anchorPos.x - width/2
|
||||
return Math.max(left, Math.min(right, want))
|
||||
}
|
||||
y: Math.max(10, root.anchorPos.y - height + 30)
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
opacity: showContextMenu ? 1 : 0
|
||||
scale: showContextMenu ? 1 : 0.85
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.appData && root.appData.isPinned ? "Unpin from Dock" : "Pin to Dock"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!root.appData) return
|
||||
if (root.appData.isPinned) {
|
||||
Prefs.removePinnedApp(root.appData.appId)
|
||||
} else {
|
||||
Prefs.addPinnedApp(root.appData.appId)
|
||||
}
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.appData && root.appData.windows && root.appData.windows.count > 0
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.appData && root.appData.windows ? root.appData.windows : null
|
||||
|
||||
Rectangle {
|
||||
required property var model
|
||||
width: menuColumn.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: model.title || "Untitled Window"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: windowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
NiriService.focusWindow(model.id)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.appData && root.appData.windows && root.appData.windows.count > 1
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.appData && root.appData.windows && root.appData.windows.count > 1
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: closeAllArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Close All Windows"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: closeAllArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeAllArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!root.appData || !root.appData.windows) return
|
||||
for (var i = 0; i < root.appData.windows.count; i++) {
|
||||
var window = root.appData.windows.get(i)
|
||||
NiriService.closeWindow(window.id)
|
||||
}
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
200
Modules/Dock/DockWindowsMenu.qml
Normal file
200
Modules/Dock/DockWindowsMenu.qml
Normal file
@@ -0,0 +1,200 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool showWindowsMenu: false
|
||||
property var appData: null
|
||||
property var anchorItem: null
|
||||
property real dockVisibleHeight: 40
|
||||
property int margin: 10
|
||||
|
||||
function showForButton(button, data, dockHeight) {
|
||||
anchorItem = button
|
||||
appData = data
|
||||
dockVisibleHeight = dockHeight || 40
|
||||
|
||||
var dockWindow = button.Window.window
|
||||
if (dockWindow) {
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
var s = Quickshell.screens[i]
|
||||
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
|
||||
root.screen = s
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showWindowsMenu = true
|
||||
}
|
||||
|
||||
function close() { showWindowsMenu = false }
|
||||
|
||||
screen: Quickshell.screens[0]
|
||||
|
||||
visible: showWindowsMenu
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
anchors { top: true; left: true; right: true; bottom: true }
|
||||
|
||||
property point anchorPos: Qt.point(screen.width/2, screen.height - 100)
|
||||
|
||||
onAnchorItemChanged: updatePosition()
|
||||
onVisibleChanged: if (visible) updatePosition()
|
||||
|
||||
function updatePosition() {
|
||||
if (!anchorItem) {
|
||||
anchorPos = Qt.point(screen.width/2, screen.height - 100)
|
||||
return
|
||||
}
|
||||
|
||||
var dockWindow = anchorItem.Window.window
|
||||
if (!dockWindow) {
|
||||
anchorPos = Qt.point(screen.width/2, screen.height - 100)
|
||||
return
|
||||
}
|
||||
|
||||
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
|
||||
|
||||
var actualDockHeight = root.dockVisibleHeight // fallback
|
||||
|
||||
function findDockBackground(item) {
|
||||
if (item.objectName === "dockBackground") {
|
||||
return item
|
||||
}
|
||||
for (var i = 0; i < item.children.length; i++) {
|
||||
var found = findDockBackground(item.children[i])
|
||||
if (found) return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
var dockBackground = findDockBackground(dockWindow.contentItem)
|
||||
if (dockBackground) {
|
||||
actualDockHeight = dockBackground.height
|
||||
}
|
||||
|
||||
var dockBottomMargin = 16 // The dock has bottom margin
|
||||
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
|
||||
|
||||
var dockContentWidth = dockWindow.width
|
||||
var screenWidth = root.screen.width
|
||||
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
|
||||
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width/2
|
||||
|
||||
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuContainer
|
||||
|
||||
width: Math.min(600, Math.max(250, windowColumn.implicitWidth + Theme.spacingS * 2))
|
||||
height: Math.max(60, windowColumn.implicitHeight + Theme.spacingS * 2)
|
||||
|
||||
x: {
|
||||
var left = 10
|
||||
var right = root.width - width - 10
|
||||
var want = root.anchorPos.x - width/2
|
||||
return Math.max(left, Math.min(right, want))
|
||||
}
|
||||
y: Math.max(10, root.anchorPos.y - height + 30)
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
opacity: showWindowsMenu ? 1 : 0
|
||||
scale: showWindowsMenu ? 1 : 0.85
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Column {
|
||||
id: windowColumn
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Repeater {
|
||||
model: root.appData && root.appData.windows ? root.appData.windows : null
|
||||
|
||||
Rectangle {
|
||||
required property var model
|
||||
width: windowColumn.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: model.title || "Untitled Window"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: model.is_focused ? Theme.primary : Theme.surfaceText
|
||||
font.weight: model.is_focused ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: windowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
NiriService.focusWindow(model.id)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
hoverEnabled: false
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user