mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(dock): extract DockContent into a reusable component and implement hover peek detection for static mode operation
This commit is contained in:
+15
-506
@@ -13,7 +13,7 @@ import qs.Widgets
|
||||
|
||||
Loader {
|
||||
|
||||
active: Settings.data.dock.enabled
|
||||
active: Settings.data.dock.enabled && Settings.data.dock.dockType !== "static"
|
||||
sourceComponent: Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
@@ -397,6 +397,10 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
property alias hideTimer: hideTimer
|
||||
property alias showTimer: showTimer
|
||||
property alias unloadTimer: unloadTimer
|
||||
|
||||
// Timer for auto-hide delay
|
||||
Timer {
|
||||
id: hideTimer
|
||||
@@ -567,8 +571,8 @@ Loader {
|
||||
readonly property int extraLeft: (!isVertical && !exclusive && barOnLeft) ? barHeight : 0
|
||||
readonly property int extraRight: (!isVertical && !exclusive && barOnRight) ? barHeight : 0
|
||||
|
||||
width: dockContainer.width + extraLeft + extraRight
|
||||
height: dockContainer.height + extraTop + extraBottom
|
||||
width: dockContent.dockContainer.width + extraLeft + extraRight
|
||||
height: dockContent.dockContainer.height + extraTop + extraBottom
|
||||
|
||||
anchors.horizontalCenter: isVertical ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: isVertical ? parent.verticalCenter : undefined
|
||||
@@ -596,509 +600,14 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dockContainer
|
||||
// For vertical dock, swap width and height logic
|
||||
width: isVertical ? Math.round(iconSize * 1.5) : Math.min(dockLayout.implicitWidth + Style.marginXL, root.maxWidth)
|
||||
height: isVertical ? Math.min(dockLayout.implicitHeight + Style.marginXL, root.maxHeight) : Math.round(iconSize * 1.5)
|
||||
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
|
||||
|
||||
// Anchor based on padding to achieve centering shift
|
||||
anchors.horizontalCenter: parent.extraLeft > 0 || parent.extraRight > 0 ? undefined : parent.horizontalCenter
|
||||
anchors.right: parent.extraLeft > 0 ? parent.right : undefined
|
||||
anchors.left: parent.extraRight > 0 ? parent.left : undefined
|
||||
|
||||
anchors.verticalCenter: parent.extraTop > 0 || parent.extraBottom > 0 ? undefined : parent.verticalCenter
|
||||
anchors.bottom: parent.extraTop > 0 ? parent.bottom : undefined
|
||||
anchors.top: parent.extraBottom > 0 ? parent.top : undefined
|
||||
|
||||
radius: Style.radiusL
|
||||
border.width: Style.borderS
|
||||
border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity)
|
||||
|
||||
// Enable layer caching to reduce GPU usage from continuous animations
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea {
|
||||
id: dockMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
dockHovered = true;
|
||||
if (autoHide) {
|
||||
showTimer.stop();
|
||||
hideTimer.stop();
|
||||
unloadTimer.stop(); // Cancel unload if hovering
|
||||
hidden = false; // Make sure dock is visible
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
dockHovered = false;
|
||||
if (autoHide && !anyAppHovered && !peekHovered && !menuHovered && root.dragSourceIndex === -1) {
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
// Close any open context menu when clicking on the dock background
|
||||
closeAllContextMenus();
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: dock
|
||||
// Use parent dimensions more directly to avoid clipping
|
||||
width: isVertical ? parent.width : Math.min(dockLayout.implicitWidth, parent.width - Style.marginXL)
|
||||
height: !isVertical ? parent.height : Math.min(dockLayout.implicitHeight, parent.height - Style.marginXL)
|
||||
contentWidth: dockLayout.implicitWidth
|
||||
contentHeight: dockLayout.implicitHeight
|
||||
anchors.centerIn: parent
|
||||
clip: true
|
||||
|
||||
flickableDirection: isVertical ? Flickable.VerticalFlick : Flickable.HorizontalFlick
|
||||
|
||||
// Keep interactive dependent on overflow
|
||||
interactive: isVertical ? contentHeight > height : contentWidth > width
|
||||
|
||||
// Centering margins
|
||||
contentX: isVertical && contentWidth < width ? (contentWidth - width) / 2 : 0
|
||||
contentY: !isVertical && contentHeight < height ? (contentHeight - height) / 2 : 0
|
||||
|
||||
WheelHandler {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => {
|
||||
var delta = (event.angleDelta.y !== 0) ? event.angleDelta.y : event.angleDelta.x;
|
||||
if (root.isVertical) {
|
||||
dock.contentY = Math.max(-dock.topMargin, Math.min(dock.contentHeight - dock.height + dock.bottomMargin, dock.contentY - delta));
|
||||
} else {
|
||||
// For horizontal dock, we want to scroll contentX with BOTH x and y wheels
|
||||
var hDelta = (event.angleDelta.x !== 0) ? event.angleDelta.x : event.angleDelta.y;
|
||||
dock.contentX = Math.max(-dock.leftMargin, Math.min(dock.contentWidth - dock.width + dock.rightMargin, dock.contentX - hDelta));
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
visible: !isVertical && dock.interactive
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
visible: isVertical && dock.interactive
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
function getAppIcon(appData): string {
|
||||
if (!appData || !appData.appId)
|
||||
return "";
|
||||
return ThemeIcons.iconForAppId(appData.appId?.toLowerCase());
|
||||
}
|
||||
|
||||
// Use GridLayout for flexible horizontal/vertical arrangement
|
||||
GridLayout {
|
||||
id: dockLayout
|
||||
columns: isVertical ? 1 : -1
|
||||
rows: isVertical ? -1 : 1
|
||||
rowSpacing: Style.marginS
|
||||
columnSpacing: Style.marginS
|
||||
|
||||
// Ensure the layout takes its full implicit size
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
Repeater {
|
||||
model: dockApps
|
||||
|
||||
delegate: Item {
|
||||
id: appButton
|
||||
readonly property real indicatorMargin: Math.max(3, Math.round(iconSize * 0.18))
|
||||
Layout.preferredWidth: isVertical ? iconSize + indicatorMargin * 2 : iconSize
|
||||
Layout.preferredHeight: isVertical ? iconSize : iconSize + indicatorMargin * 2
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
property bool isActive: modelData.toplevel && ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData.toplevel
|
||||
property bool hovered: appMouseArea.containsMouse
|
||||
property string appId: modelData ? modelData.appId : ""
|
||||
property string appTitle: {
|
||||
if (!modelData)
|
||||
return "";
|
||||
// For running apps, use the toplevel title directly (reactive)
|
||||
if (modelData.toplevel) {
|
||||
const toplevelTitle = modelData.toplevel.title || "";
|
||||
// If title is "Loading..." or empty, use desktop entry name
|
||||
if (!toplevelTitle || toplevelTitle === "Loading..." || toplevelTitle.trim() === "") {
|
||||
return root.getAppNameFromDesktopEntry(modelData.appId) || modelData.appId;
|
||||
}
|
||||
return toplevelTitle;
|
||||
}
|
||||
// For pinned apps that aren't running, use the stored title
|
||||
return modelData.title || modelData.appId || "";
|
||||
}
|
||||
property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running")
|
||||
|
||||
// Store index for drag-and-drop
|
||||
property int modelIndex: index
|
||||
objectName: "dockAppButton"
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
keys: ["dock-app"]
|
||||
onEntered: function (drag) {
|
||||
if (drag.source && drag.source.objectName === "dockAppButton") {
|
||||
root.dragTargetIndex = appButton.modelIndex;
|
||||
}
|
||||
}
|
||||
onExited: function () {
|
||||
if (root.dragTargetIndex === appButton.modelIndex) {
|
||||
root.dragTargetIndex = -1;
|
||||
}
|
||||
}
|
||||
onDropped: function (drop) {
|
||||
root.dragSourceIndex = -1;
|
||||
root.dragTargetIndex = -1;
|
||||
if (drop.source && drop.source.objectName === "dockAppButton" && drop.source !== appButton) {
|
||||
root.reorderApps(drop.source.modelIndex, appButton.modelIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for the toplevel being closed
|
||||
Connections {
|
||||
target: modelData?.toplevel
|
||||
function onClosed() {
|
||||
Qt.callLater(root.updateDockApps);
|
||||
}
|
||||
}
|
||||
|
||||
// Draggable container for the icon
|
||||
Item {
|
||||
id: iconContainer
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
|
||||
// When dragging, remove anchors so MouseArea can position it
|
||||
anchors.centerIn: dragging ? undefined : parent
|
||||
|
||||
property bool dragging: appMouseArea.drag.active
|
||||
onDraggingChanged: {
|
||||
if (dragging) {
|
||||
root.dragSourceIndex = index;
|
||||
} else {
|
||||
// Reset if not handled by drop (e.g. dropped outside)
|
||||
Qt.callLater(() => {
|
||||
if (!appMouseArea.drag.active && root.dragSourceIndex === index) {
|
||||
root.dragSourceIndex = -1;
|
||||
root.dragTargetIndex = -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Drag.active: dragging
|
||||
Drag.source: appButton
|
||||
Drag.hotSpot.x: width / 2
|
||||
Drag.hotSpot.y: height / 2
|
||||
Drag.keys: ["dock-app"]
|
||||
|
||||
z: (root.dragSourceIndex === index) ? 1000 : ((dragging ? 1000 : 0))
|
||||
scale: dragging ? 1.1 : (appButton.hovered ? 1.15 : 1.0)
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
// Visual shifting logic
|
||||
readonly property bool isDragged: root.dragSourceIndex === index
|
||||
property real shiftOffset: 0
|
||||
|
||||
Binding on shiftOffset {
|
||||
value: {
|
||||
if (root.dragSourceIndex !== -1 && root.dragTargetIndex !== -1 && !iconContainer.isDragged) {
|
||||
if (root.dragSourceIndex < root.dragTargetIndex) {
|
||||
// Dragging Forward: Items between source and target shift Backward
|
||||
if (index > root.dragSourceIndex && index <= root.dragTargetIndex) {
|
||||
return -1 * (root.isVertical ? iconSize + Style.marginS : iconSize + Style.marginS);
|
||||
}
|
||||
} else if (root.dragSourceIndex > root.dragTargetIndex) {
|
||||
// Dragging Backward: Items between target and source shift Forward
|
||||
if (index >= root.dragTargetIndex && index < root.dragSourceIndex) {
|
||||
return (root.isVertical ? iconSize + Style.marginS : iconSize + Style.marginS);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: !root.isVertical ? iconContainer.shiftOffset : 0
|
||||
y: root.isVertical ? iconContainer.shiftOffset : 0
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
root.iconRevision; // Force re-evaluation when revision changes
|
||||
return dock.getAppIcon(modelData);
|
||||
}
|
||||
visible: source.toString() !== ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
|
||||
// Dim pinned apps that aren't running
|
||||
opacity: appButton.isRunning ? 1.0 : Settings.data.dock.deadOpacity
|
||||
|
||||
// Apply dock-specific colorization shader only to non-focused apps
|
||||
layer.enabled: !appButton.isActive && Settings.data.dock.colorizeIcons
|
||||
layer.effect: ShaderEffect {
|
||||
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
|
||||
property real colorizeMode: 0.0 // Dock mode (grayscale)
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back if no icon
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: !appIcon.visible
|
||||
icon: "question-mark"
|
||||
pointSize: iconSize * 0.7
|
||||
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
opacity: appButton.isRunning ? 1.0 : 0.6
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu popup
|
||||
DockMenu {
|
||||
id: contextMenu
|
||||
dockPosition: root.dockPosition // Pass dock position for menu placement
|
||||
onHoveredChanged: {
|
||||
// Only update menuHovered if this menu is current and visible
|
||||
if (root.currentContextMenu === contextMenu && contextMenu.visible) {
|
||||
menuHovered = hovered;
|
||||
} else {
|
||||
menuHovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contextMenu
|
||||
function onRequestClose() {
|
||||
// Clear current menu immediately to prevent hover updates
|
||||
root.currentContextMenu = null;
|
||||
hideTimer.stop();
|
||||
contextMenu.hide();
|
||||
menuHovered = false;
|
||||
anyAppHovered = false;
|
||||
}
|
||||
}
|
||||
onAppClosed: root.updateDockApps // Force immediate dock update when app is closed
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
root.currentContextMenu = contextMenu;
|
||||
} else if (root.currentContextMenu === contextMenu) {
|
||||
root.currentContextMenu = null;
|
||||
hideTimer.stop();
|
||||
menuHovered = false;
|
||||
// Restart hide timer after menu closes
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: appMouseArea
|
||||
objectName: "appMouseArea"
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
|
||||
// Only allow left-click dragging via axis control
|
||||
drag.target: iconContainer
|
||||
drag.axis: (pressedButtons & Qt.LeftButton) ? (root.isVertical ? Drag.YAxis : Drag.XAxis) : Drag.None
|
||||
|
||||
onPressed: {
|
||||
var p1 = appButton.mapFromItem(dockContainer, 0, 0);
|
||||
var p2 = appButton.mapFromItem(dockContainer, dockContainer.width, dockContainer.height);
|
||||
drag.minimumX = p1.x;
|
||||
drag.maximumX = p2.x - iconContainer.width;
|
||||
drag.minimumY = p1.y;
|
||||
drag.maximumY = p2.y - iconContainer.height;
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (iconContainer.Drag.active) {
|
||||
iconContainer.Drag.drop();
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
anyAppHovered = true;
|
||||
const appName = appButton.appTitle || appButton.appId || "Unknown";
|
||||
const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName;
|
||||
if (!contextMenu.visible) {
|
||||
TooltipService.show(appButton, tooltipText, "top");
|
||||
}
|
||||
if (autoHide) {
|
||||
showTimer.stop();
|
||||
hideTimer.stop();
|
||||
unloadTimer.stop(); // Cancel unload if hovering app
|
||||
hidden = false; // Make sure dock is visible
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
anyAppHovered = false;
|
||||
TooltipService.hide();
|
||||
// Clear menuHovered if no current menu or menu not visible
|
||||
if (!root.currentContextMenu || !root.currentContextMenu.visible) {
|
||||
menuHovered = false;
|
||||
}
|
||||
if (autoHide && !dockHovered && !peekHovered && !menuHovered && root.dragSourceIndex === -1) {
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
// If right-clicking on the same app with an open context menu, close it
|
||||
if (root.currentContextMenu === contextMenu && contextMenu.visible) {
|
||||
root.closeAllContextMenus();
|
||||
return;
|
||||
}
|
||||
// Close any other existing context menu first
|
||||
root.closeAllContextMenus();
|
||||
// Hide tooltip when showing context menu
|
||||
TooltipService.hideImmediately();
|
||||
contextMenu.show(appButton, modelData.toplevel || modelData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any existing context menu for non-right-click actions
|
||||
root.closeAllContextMenus();
|
||||
|
||||
// Check if toplevel is still valid (not a stale reference)
|
||||
const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel);
|
||||
|
||||
if (mouse.button === Qt.MiddleButton && isValidToplevel && modelData.toplevel.close) {
|
||||
modelData.toplevel.close();
|
||||
Qt.callLater(root.updateDockApps); // Force immediate dock update
|
||||
} else if (mouse.button === Qt.LeftButton) {
|
||||
if (isValidToplevel && modelData.toplevel.activate) {
|
||||
// Running app - activate it
|
||||
modelData.toplevel.activate();
|
||||
} else if (modelData?.appId) {
|
||||
// Pinned app not running - launch it
|
||||
// Use ThemeIcons to robustly find the desktop entry
|
||||
const app = ThemeIcons.findAppEntry(modelData.appId);
|
||||
|
||||
if (!app) {
|
||||
Logger.w("Dock", `Could not find desktop entry for pinned app: ${modelData.appId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) {
|
||||
// Use custom launch prefix
|
||||
const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" ");
|
||||
|
||||
if (app.runInTerminal) {
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = prefix.concat(terminal.concat(app.command));
|
||||
Quickshell.execDetached(command);
|
||||
} else {
|
||||
const command = prefix.concat(app.command);
|
||||
Quickshell.execDetached(command);
|
||||
}
|
||||
} else if (Settings.data.appLauncher.useApp2Unit && ProgramCheckerService.app2unitAvailable && app.id) {
|
||||
Logger.d("Dock", `Using app2unit for: ${app.id}`);
|
||||
if (app.runInTerminal)
|
||||
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"]);
|
||||
else
|
||||
Quickshell.execDetached(["app2unit", "--"].concat(app.command));
|
||||
} else {
|
||||
// Fallback logic when app2unit is not used
|
||||
if (app.runInTerminal) {
|
||||
Logger.d("Dock", "Executing terminal app manually: " + app.name);
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = terminal.concat(app.command);
|
||||
CompositorService.spawn(command);
|
||||
} else if (app.command && app.command.length > 0) {
|
||||
CompositorService.spawn(app.command);
|
||||
} else if (app.execute) {
|
||||
app.execute();
|
||||
} else {
|
||||
Logger.w("Dock", `Could not launch: ${app.name}. No valid launch method.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Active indicator - positioned at the edge of the delegate area
|
||||
Rectangle {
|
||||
visible: Settings.data.dock.inactiveIndicators ? isRunning : isActive
|
||||
width: isVertical ? indicatorMargin * 0.6 : iconSize * 0.2
|
||||
height: isVertical ? iconSize * 0.2 : indicatorMargin * 0.6
|
||||
color: Color.mPrimary
|
||||
radius: Style.radiusXS
|
||||
|
||||
// Anchor to the edge facing the screen center
|
||||
anchors.bottom: !isVertical && dockPosition === "bottom" ? parent.bottom : undefined
|
||||
anchors.top: !isVertical && dockPosition === "top" ? parent.top : undefined
|
||||
anchors.left: isVertical && dockPosition === "left" ? parent.left : undefined
|
||||
anchors.right: isVertical && dockPosition === "right" ? parent.right : undefined
|
||||
|
||||
anchors.horizontalCenter: isVertical ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: isVertical ? parent.verticalCenter : undefined
|
||||
|
||||
// Offset slightly from the edge
|
||||
anchors.bottomMargin: !isVertical && dockPosition === "bottom" ? 2 : 0
|
||||
anchors.topMargin: !isVertical && dockPosition === "top" ? 2 : 0
|
||||
anchors.leftMargin: isVertical && dockPosition === "left" ? 2 : 0
|
||||
anchors.rightMargin: isVertical && dockPosition === "right" ? 2 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DockContent {
|
||||
id: dockContent
|
||||
anchors.fill: parent
|
||||
dockRoot: root
|
||||
extraTop: dockContainerWrapper.extraTop
|
||||
extraBottom: dockContainerWrapper.extraBottom
|
||||
extraLeft: dockContainerWrapper.extraLeft
|
||||
extraRight: dockContainerWrapper.extraRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
required property var dockRoot
|
||||
required property int extraTop
|
||||
required property int extraBottom
|
||||
required property int extraLeft
|
||||
required property int extraRight
|
||||
property alias dockContainer: dockContainer
|
||||
|
||||
Rectangle {
|
||||
id: dockContainer
|
||||
// For vertical dock, swap width and height logic
|
||||
width: dockRoot.isVertical ? Math.round(dockRoot.iconSize * 1.5) : Math.min(dockLayout.implicitWidth + Style.marginXL, dockRoot.maxWidth)
|
||||
height: dockRoot.isVertical ? Math.min(dockLayout.implicitHeight + Style.marginXL, dockRoot.maxHeight) : Math.round(dockRoot.iconSize * 1.5)
|
||||
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
|
||||
|
||||
// Anchor based on padding to achieve centering shift
|
||||
anchors.horizontalCenter: extraLeft > 0 || extraRight > 0 ? undefined : parent.horizontalCenter
|
||||
anchors.right: extraLeft > 0 ? parent.right : undefined
|
||||
anchors.left: extraRight > 0 ? parent.left : undefined
|
||||
|
||||
anchors.verticalCenter: extraTop > 0 || extraBottom > 0 ? undefined : parent.verticalCenter
|
||||
anchors.bottom: extraTop > 0 ? parent.bottom : undefined
|
||||
anchors.top: extraBottom > 0 ? parent.top : undefined
|
||||
|
||||
radius: Style.radiusL
|
||||
border.width: Style.borderS
|
||||
border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity)
|
||||
|
||||
// Enable layer caching to reduce GPU usage from continuous animations
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea {
|
||||
id: dockMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
dockRoot.dockHovered = true;
|
||||
if (dockRoot.autoHide) {
|
||||
dockRoot.showTimer.stop();
|
||||
dockRoot.hideTimer.stop();
|
||||
dockRoot.unloadTimer.stop(); // Cancel unload if hovering
|
||||
dockRoot.hidden = false; // Make sure dock is visible
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
dockRoot.dockHovered = false;
|
||||
if (dockRoot.autoHide && !dockRoot.anyAppHovered && !dockRoot.peekHovered && !dockRoot.menuHovered && dockRoot.dragSourceIndex === -1) {
|
||||
dockRoot.hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
// Close any open context menu when clicking on the dock background
|
||||
dockRoot.closeAllContextMenus();
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: dock
|
||||
// Use parent dimensions more directly to avoid clipping
|
||||
width: dockRoot.isVertical ? parent.width : Math.min(dockLayout.implicitWidth, parent.width - Style.marginXL)
|
||||
height: !dockRoot.isVertical ? parent.height : Math.min(dockLayout.implicitHeight, parent.height - Style.marginXL)
|
||||
contentWidth: dockLayout.implicitWidth
|
||||
contentHeight: dockLayout.implicitHeight
|
||||
anchors.centerIn: parent
|
||||
clip: true
|
||||
|
||||
flickableDirection: dockRoot.isVertical ? Flickable.VerticalFlick : Flickable.HorizontalFlick
|
||||
|
||||
// Keep interactive dependent on overflow
|
||||
interactive: dockRoot.isVertical ? contentHeight > height : contentWidth > width
|
||||
|
||||
// Centering margins
|
||||
contentX: dockRoot.isVertical && contentWidth < width ? (contentWidth - width) / 2 : 0
|
||||
contentY: !dockRoot.isVertical && contentHeight < height ? (contentHeight - height) / 2 : 0
|
||||
|
||||
WheelHandler {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => {
|
||||
var delta = (event.angleDelta.y !== 0) ? event.angleDelta.y : event.angleDelta.x;
|
||||
if (dockRoot.isVertical) {
|
||||
dock.contentY = Math.max(-dock.topMargin, Math.min(dock.contentHeight - dock.height + dock.bottomMargin, dock.contentY - delta));
|
||||
} else {
|
||||
// For horizontal dock, we want to scroll contentX with BOTH x and y wheels
|
||||
var hDelta = (event.angleDelta.x !== 0) ? event.angleDelta.x : event.angleDelta.y;
|
||||
dock.contentX = Math.max(-dock.leftMargin, Math.min(dock.contentWidth - dock.width + dock.rightMargin, dock.contentX - hDelta));
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
visible: !dockRoot.isVertical && dock.interactive
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
visible: dockRoot.isVertical && dock.interactive
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
function getAppIcon(appData): string {
|
||||
if (!appData || !appData.appId)
|
||||
return "";
|
||||
return ThemeIcons.iconForAppId(appData.appId?.toLowerCase());
|
||||
}
|
||||
|
||||
// Use GridLayout for flexible horizontal/vertical arrangement
|
||||
GridLayout {
|
||||
id: dockLayout
|
||||
columns: dockRoot.isVertical ? 1 : -1
|
||||
rows: dockRoot.isVertical ? -1 : 1
|
||||
rowSpacing: Style.marginS
|
||||
columnSpacing: Style.marginS
|
||||
|
||||
// Ensure the layout takes its full implicit size
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
Repeater {
|
||||
model: dockRoot.dockApps
|
||||
|
||||
delegate: Item {
|
||||
id: appButton
|
||||
readonly property real indicatorMargin: Math.max(3, Math.round(dockRoot.iconSize * 0.18))
|
||||
Layout.preferredWidth: dockRoot.isVertical ? dockRoot.iconSize + indicatorMargin * 2 : dockRoot.iconSize
|
||||
Layout.preferredHeight: dockRoot.isVertical ? dockRoot.iconSize : dockRoot.iconSize + indicatorMargin * 2
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
property bool isActive: modelData.toplevel && ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData.toplevel
|
||||
property bool hovered: appMouseArea.containsMouse
|
||||
property string appId: modelData ? modelData.appId : ""
|
||||
property string appTitle: {
|
||||
if (!modelData)
|
||||
return "";
|
||||
// For running apps, use the toplevel title directly (reactive)
|
||||
if (modelData.toplevel) {
|
||||
const toplevelTitle = modelData.toplevel.title || "";
|
||||
// If title is "Loading..." or empty, use desktop entry name
|
||||
if (!toplevelTitle || toplevelTitle === "Loading..." || toplevelTitle.trim() === "") {
|
||||
return dockRoot.getAppNameFromDesktopEntry(modelData.appId) || modelData.appId;
|
||||
}
|
||||
return toplevelTitle;
|
||||
}
|
||||
// For pinned apps that aren't running, use the stored title
|
||||
return modelData.title || modelData.appId || "";
|
||||
}
|
||||
property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running")
|
||||
|
||||
// Store index for drag-and-drop
|
||||
property int modelIndex: index
|
||||
objectName: "dockAppButton"
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
keys: ["dock-app"]
|
||||
onEntered: function (drag) {
|
||||
if (drag.source && drag.source.objectName === "dockAppButton") {
|
||||
dockRoot.dragTargetIndex = appButton.modelIndex;
|
||||
}
|
||||
}
|
||||
onExited: function () {
|
||||
if (dockRoot.dragTargetIndex === appButton.modelIndex) {
|
||||
dockRoot.dragTargetIndex = -1;
|
||||
}
|
||||
}
|
||||
onDropped: function (drop) {
|
||||
dockRoot.dragSourceIndex = -1;
|
||||
dockRoot.dragTargetIndex = -1;
|
||||
if (drop.source && drop.source.objectName === "dockAppButton" && drop.source !== appButton) {
|
||||
dockRoot.reorderApps(drop.source.modelIndex, appButton.modelIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for the toplevel being closed
|
||||
Connections {
|
||||
target: modelData?.toplevel
|
||||
function onClosed() {
|
||||
Qt.callLater(dockRoot.updateDockApps);
|
||||
}
|
||||
}
|
||||
|
||||
// Draggable container for the icon
|
||||
Item {
|
||||
id: iconContainer
|
||||
width: dockRoot.iconSize
|
||||
height: dockRoot.iconSize
|
||||
|
||||
// When dragging, remove anchors so MouseArea can position it
|
||||
anchors.centerIn: dragging ? undefined : parent
|
||||
|
||||
property bool dragging: appMouseArea.drag.active
|
||||
onDraggingChanged: {
|
||||
if (dragging) {
|
||||
dockRoot.dragSourceIndex = index;
|
||||
} else {
|
||||
// Reset if not handled by drop (e.g. dropped outside)
|
||||
Qt.callLater(() => {
|
||||
if (!appMouseArea.drag.active && dockRoot.dragSourceIndex === index) {
|
||||
dockRoot.dragSourceIndex = -1;
|
||||
dockRoot.dragTargetIndex = -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Drag.active: dragging
|
||||
Drag.source: appButton
|
||||
Drag.hotSpot.x: width / 2
|
||||
Drag.hotSpot.y: height / 2
|
||||
Drag.keys: ["dock-app"]
|
||||
|
||||
z: (dockRoot.dragSourceIndex === index) ? 1000 : ((dragging ? 1000 : 0))
|
||||
scale: dragging ? 1.1 : (appButton.hovered ? 1.15 : 1.0)
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
// Visual shifting logic
|
||||
readonly property bool isDragged: dockRoot.dragSourceIndex === index
|
||||
property real shiftOffset: 0
|
||||
|
||||
Binding on shiftOffset {
|
||||
value: {
|
||||
if (dockRoot.dragSourceIndex !== -1 && dockRoot.dragTargetIndex !== -1 && !iconContainer.isDragged) {
|
||||
if (dockRoot.dragSourceIndex < dockRoot.dragTargetIndex) {
|
||||
// Dragging Forward: Items between source and target shift Backward
|
||||
if (index > dockRoot.dragSourceIndex && index <= dockRoot.dragTargetIndex) {
|
||||
return -1 * (dockRoot.isVertical ? dockRoot.iconSize + Style.marginS : dockRoot.iconSize + Style.marginS);
|
||||
}
|
||||
} else if (dockRoot.dragSourceIndex > dockRoot.dragTargetIndex) {
|
||||
// Dragging Backward: Items between target and source shift Forward
|
||||
if (index >= dockRoot.dragTargetIndex && index < dockRoot.dragSourceIndex) {
|
||||
return (dockRoot.isVertical ? dockRoot.iconSize + Style.marginS : dockRoot.iconSize + Style.marginS);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: !dockRoot.isVertical ? iconContainer.shiftOffset : 0
|
||||
y: dockRoot.isVertical ? iconContainer.shiftOffset : 0
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
dockRoot.iconRevision; // Force re-evaluation when revision changes
|
||||
return dock.getAppIcon(modelData);
|
||||
}
|
||||
visible: source.toString() !== ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
|
||||
// Dim pinned apps that aren't running
|
||||
opacity: appButton.isRunning ? 1.0 : Settings.data.dock.deadOpacity
|
||||
|
||||
// Apply dock-specific colorization shader only to non-focused apps
|
||||
layer.enabled: !appButton.isActive && Settings.data.dock.colorizeIcons
|
||||
layer.effect: ShaderEffect {
|
||||
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
|
||||
property real colorizeMode: 0.0 // Dock mode (grayscale)
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back if no icon
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: !appIcon.visible
|
||||
icon: "question-mark"
|
||||
pointSize: dockRoot.iconSize * 0.7
|
||||
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
opacity: appButton.isRunning ? 1.0 : 0.6
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu popup
|
||||
DockMenu {
|
||||
id: contextMenu
|
||||
dockPosition: dockRoot.dockPosition // Pass dock position for menu placement
|
||||
onHoveredChanged: {
|
||||
// Only update menuHovered if this menu is current and visible
|
||||
if (dockRoot.currentContextMenu === contextMenu && contextMenu.visible) {
|
||||
dockRoot.menuHovered = hovered;
|
||||
} else {
|
||||
dockRoot.menuHovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contextMenu
|
||||
function onRequestClose() {
|
||||
// Clear current menu immediately to prevent hover updates
|
||||
dockRoot.currentContextMenu = null;
|
||||
dockRoot.hideTimer.stop();
|
||||
contextMenu.hide();
|
||||
dockRoot.menuHovered = false;
|
||||
dockRoot.anyAppHovered = false;
|
||||
}
|
||||
}
|
||||
onAppClosed: dockRoot.updateDockApps // Force immediate dock update when app is closed
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
dockRoot.currentContextMenu = contextMenu;
|
||||
} else if (dockRoot.currentContextMenu === contextMenu) {
|
||||
dockRoot.currentContextMenu = null;
|
||||
dockRoot.hideTimer.stop();
|
||||
dockRoot.menuHovered = false;
|
||||
// Restart hide timer after menu closes
|
||||
if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.anyAppHovered && !dockRoot.peekHovered && !dockRoot.menuHovered) {
|
||||
dockRoot.hideTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: appMouseArea
|
||||
objectName: "appMouseArea"
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
|
||||
// Only allow left-click dragging via axis control
|
||||
drag.target: iconContainer
|
||||
drag.axis: (pressedButtons & Qt.LeftButton) ? (dockRoot.isVertical ? Drag.YAxis : Drag.XAxis) : Drag.None
|
||||
|
||||
onPressed: {
|
||||
var p1 = appButton.mapFromItem(dockContainer, 0, 0);
|
||||
var p2 = appButton.mapFromItem(dockContainer, dockContainer.width, dockContainer.height);
|
||||
drag.minimumX = p1.x;
|
||||
drag.maximumX = p2.x - iconContainer.width;
|
||||
drag.minimumY = p1.y;
|
||||
drag.maximumY = p2.y - iconContainer.height;
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (iconContainer.Drag.active) {
|
||||
iconContainer.Drag.drop();
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
dockRoot.anyAppHovered = true;
|
||||
const appName = appButton.appTitle || appButton.appId || "Unknown";
|
||||
const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName;
|
||||
if (!contextMenu.visible) {
|
||||
TooltipService.show(appButton, tooltipText, "top");
|
||||
}
|
||||
if (dockRoot.autoHide) {
|
||||
dockRoot.showTimer.stop();
|
||||
dockRoot.hideTimer.stop();
|
||||
dockRoot.unloadTimer.stop(); // Cancel unload if hovering app
|
||||
dockRoot.hidden = false; // Make sure dock is visible
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
dockRoot.anyAppHovered = false;
|
||||
TooltipService.hide();
|
||||
// Clear menuHovered if no current menu or menu not visible
|
||||
if (!dockRoot.currentContextMenu || !dockRoot.currentContextMenu.visible) {
|
||||
dockRoot.menuHovered = false;
|
||||
}
|
||||
if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.peekHovered && !dockRoot.menuHovered && dockRoot.dragSourceIndex === -1) {
|
||||
dockRoot.hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
// If right-clicking on the same app with an open context menu, close it
|
||||
if (dockRoot.currentContextMenu === contextMenu && contextMenu.visible) {
|
||||
dockRoot.closeAllContextMenus();
|
||||
return;
|
||||
}
|
||||
// Close any other existing context menu first
|
||||
dockRoot.closeAllContextMenus();
|
||||
// Hide tooltip when showing context menu
|
||||
TooltipService.hideImmediately();
|
||||
contextMenu.show(appButton, modelData.toplevel || modelData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any existing context menu for non-right-click actions
|
||||
dockRoot.closeAllContextMenus();
|
||||
|
||||
// Check if toplevel is still valid (not a stale reference)
|
||||
const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel);
|
||||
|
||||
if (mouse.button === Qt.MiddleButton && isValidToplevel && modelData.toplevel.close) {
|
||||
modelData.toplevel.close();
|
||||
Qt.callLater(dockRoot.updateDockApps); // Force immediate dock update
|
||||
} else if (mouse.button === Qt.LeftButton) {
|
||||
if (isValidToplevel && modelData.toplevel.activate) {
|
||||
// Running app - activate it
|
||||
modelData.toplevel.activate();
|
||||
} else if (modelData?.appId) {
|
||||
// Pinned app not running - launch it
|
||||
// Use ThemeIcons to robustly find the desktop entry
|
||||
const app = ThemeIcons.findAppEntry(modelData.appId);
|
||||
|
||||
if (!app) {
|
||||
Logger.w("Dock", `Could not find desktop entry for pinned app: ${modelData.appId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) {
|
||||
// Use custom launch prefix
|
||||
const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" ");
|
||||
|
||||
if (app.runInTerminal) {
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = prefix.concat(terminal.concat(app.command));
|
||||
Quickshell.execDetached(command);
|
||||
} else {
|
||||
const command = prefix.concat(app.command);
|
||||
Quickshell.execDetached(command);
|
||||
}
|
||||
} else if (Settings.data.appLauncher.useApp2Unit && ProgramCheckerService.app2unitAvailable && app.id) {
|
||||
Logger.d("Dock", `Using app2unit for: ${app.id}`);
|
||||
if (app.runInTerminal)
|
||||
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"]);
|
||||
else
|
||||
Quickshell.execDetached(["app2unit", "--"].concat(app.command));
|
||||
} else {
|
||||
// Fallback logic when app2unit is not used
|
||||
if (app.runInTerminal) {
|
||||
Logger.d("Dock", "Executing terminal app manually: " + app.name);
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = terminal.concat(app.command);
|
||||
CompositorService.spawn(command);
|
||||
} else if (app.command && app.command.length > 0) {
|
||||
CompositorService.spawn(app.command);
|
||||
} else if (app.execute) {
|
||||
app.execute();
|
||||
} else {
|
||||
Logger.w("Dock", `Could not launch: ${app.name}. No valid launch method.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Active indicator - positioned at the edge of the delegate area
|
||||
Rectangle {
|
||||
visible: Settings.data.dock.inactiveIndicators ? isRunning : isActive
|
||||
width: dockRoot.isVertical ? indicatorMargin * 0.6 : dockRoot.iconSize * 0.2
|
||||
height: dockRoot.isVertical ? dockRoot.iconSize * 0.2 : indicatorMargin * 0.6
|
||||
color: Color.mPrimary
|
||||
radius: Style.radiusXS
|
||||
|
||||
// Anchor to the edge facing the screen center
|
||||
anchors.bottom: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? parent.bottom : undefined
|
||||
anchors.top: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? parent.top : undefined
|
||||
anchors.left: dockRoot.isVertical && dockRoot.dockPosition === "left" ? parent.left : undefined
|
||||
anchors.right: dockRoot.isVertical && dockRoot.dockPosition === "right" ? parent.right : undefined
|
||||
|
||||
anchors.horizontalCenter: dockRoot.isVertical ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: dockRoot.isVertical ? parent.verticalCenter : undefined
|
||||
|
||||
// Offset slightly from the edge
|
||||
anchors.bottomMargin: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? 2 : 0
|
||||
anchors.topMargin: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? 2 : 0
|
||||
anchors.leftMargin: dockRoot.isVertical && dockRoot.dockPosition === "left" ? 2 : 0
|
||||
anchors.rightMargin: dockRoot.isVertical && dockRoot.dockPosition === "right" ? 2 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
|
||||
@@ -11,6 +12,7 @@ SmartPanel {
|
||||
|
||||
readonly property string dockPosition: Settings.data.dock.position
|
||||
readonly property bool isVertical: dockPosition === "left" || dockPosition === "right"
|
||||
readonly property bool isStaticMode: Settings.data.dock.dockType === "static"
|
||||
|
||||
panelAnchorTop: dockPosition === "top"
|
||||
panelAnchorBottom: dockPosition === "bottom"
|
||||
@@ -22,14 +24,75 @@ SmartPanel {
|
||||
forceAttachToBar: true
|
||||
exclusiveKeyboard: false
|
||||
|
||||
preferredWidth: Math.max(1, dockWidth)
|
||||
preferredHeight: Math.max(1, dockHeight)
|
||||
// Fixed size 200x200
|
||||
preferredWidth: 200
|
||||
preferredHeight: 200
|
||||
|
||||
// Peek Window to detect hover when panel is closed
|
||||
Loader {
|
||||
active: root.isStaticMode && !root.isPanelOpen && !root.isClosing && root.screen
|
||||
sourceComponent: PanelWindow {
|
||||
id: peekWindow
|
||||
screen: root.screen
|
||||
color: "transparent"
|
||||
focusable: false
|
||||
|
||||
// Layer config
|
||||
WlrLayershell.namespace: "noctalia-static-dock-peek-" + (screen?.name || "unknown")
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// implicitHeight: barAtSameEdge && !isVertical ? 3 : peekHeight
|
||||
// implicitWidth: barAtSameEdge && isVertical ? 3 : peekHeight
|
||||
|
||||
// Anchors
|
||||
anchors.top: root.dockPosition === "top" || root.isVertical
|
||||
anchors.bottom: root.dockPosition === "bottom" || root.isVertical
|
||||
anchors.left: root.dockPosition === "left" || !root.isVertical
|
||||
anchors.right: root.dockPosition === "right" || !root.isVertical
|
||||
|
||||
// Size - 2px thick strip
|
||||
implicitWidth: root.isVertical ? 2 : (root.screen ? Math.round(root.screen.width) : 0)
|
||||
implicitHeight: !root.isVertical ? 2 : (root.screen ? Math.round(root.screen.height) : 0)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
root.open(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panelContent: Item {
|
||||
id: panelContent
|
||||
|
||||
property bool allowAttach: true
|
||||
property real contentPreferredWidth: Math.max(1, root.dockWidth)
|
||||
property real contentPreferredHeight: Math.max(1, root.dockHeight)
|
||||
property real contentPreferredWidth: 300
|
||||
property real contentPreferredHeight: 50 - Settings.data.bar.frameThickness
|
||||
|
||||
// Detect mouse exit to close panel
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onExited: {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent.centerIn
|
||||
color: "darkred"
|
||||
radius: 24
|
||||
width: 300
|
||||
height: 50
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Static Dock"
|
||||
color: Settings.data.colorSchemes.darkMode ? "#cdd6f4" : "#4c4f69"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user