feat: add multi-instance window hover preview cards to bottom dock
This commit is contained in:
parent
5cb7679b59
commit
360f96c653
@ -197,6 +197,27 @@ Item {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
|
||||||
|
onEntered: {
|
||||||
|
if (TaskService.isRunning(modelData.appId)) {
|
||||||
|
let instances = TaskService.getWindowsForApp(modelData.appId)
|
||||||
|
if (instances && instances.length > 0) {
|
||||||
|
root.previewTargetApp = modelData
|
||||||
|
root.previewWindowInstances = instances
|
||||||
|
root.previewTargetX = Math.round(dockItem.mapToItem(null, 0, 0).x)
|
||||||
|
root.isPreviewHovered = true
|
||||||
|
previewCloseTimer.stop()
|
||||||
|
previewPopOut.running = false
|
||||||
|
dockWindowPreview.visible = true
|
||||||
|
previewPopIn.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: {
|
||||||
|
root.isPreviewHovered = false
|
||||||
|
previewCloseTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
property real dragXOffset: 0
|
property real dragXOffset: 0
|
||||||
property real pressStartX: 0
|
property real pressStartX: 0
|
||||||
property bool isDragActive: false
|
property bool isDragActive: false
|
||||||
@ -414,6 +435,279 @@ Item {
|
|||||||
|
|
||||||
property var dockWindow: null
|
property var dockWindow: null
|
||||||
|
|
||||||
|
// Window Preview Hover State Properties
|
||||||
|
property var previewTargetApp: null
|
||||||
|
property var previewWindowInstances: []
|
||||||
|
property int previewTargetX: 0
|
||||||
|
property bool isPreviewHovered: false
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: previewCloseTimer
|
||||||
|
interval: 220
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
if (!root.isPreviewHovered && !previewMouseArea.containsMouse) {
|
||||||
|
previewPopIn.running = false
|
||||||
|
previewPopOut.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live Window Preview Floating Popup Window
|
||||||
|
PopupWindow {
|
||||||
|
id: dockWindowPreview
|
||||||
|
anchor.window: root.dockWindow
|
||||||
|
anchor.rect.x: Math.max(10, Math.min(root.dockWindow ? root.dockWindow.width - previewGlass.implicitWidth - 10 : 800, root.previewTargetX - Math.round(previewGlass.implicitWidth / 2) + 21))
|
||||||
|
anchor.rect.y: 0
|
||||||
|
anchor.edges: Edges.Top | Edges.Left
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
implicitWidth: previewGlass.implicitWidth
|
||||||
|
implicitHeight: previewGlass.implicitHeight
|
||||||
|
|
||||||
|
property real animProgress: 0.0
|
||||||
|
|
||||||
|
NumberAnimation on animProgress {
|
||||||
|
id: previewPopIn
|
||||||
|
running: false
|
||||||
|
to: 1.0
|
||||||
|
duration: 220
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
easing.overshoot: 1.15
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation on animProgress {
|
||||||
|
id: previewPopOut
|
||||||
|
running: false
|
||||||
|
to: 0.0
|
||||||
|
duration: 160
|
||||||
|
easing.type: Easing.InQuad
|
||||||
|
onFinished: dockWindowPreview.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: previewMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onEntered: previewCloseTimer.stop()
|
||||||
|
onExited: previewCloseTimer.start()
|
||||||
|
|
||||||
|
GlassPanel {
|
||||||
|
id: previewGlass
|
||||||
|
implicitWidth: previewMainCol.implicitWidth + 20
|
||||||
|
implicitHeight: previewMainCol.implicitHeight + 16
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
opacity: dockWindowPreview.animProgress
|
||||||
|
scale: 0.90 + 0.10 * dockWindowPreview.animProgress
|
||||||
|
transformOrigin: Item.Bottom
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: previewMainCol
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
// Header: App Title & Running Count
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 14
|
||||||
|
height: 14
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceSize.width: 14
|
||||||
|
sourceSize.height: 14
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
source: root.getIconPath(root.previewTargetApp ? root.previewTargetApp.icon : "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.previewTargetApp ? root.getCleanProgramName(root.previewTargetApp) : "Windows"
|
||||||
|
color: Theme.fg
|
||||||
|
font.pixelSize: 11
|
||||||
|
font.weight: Font.Bold
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
height: 16
|
||||||
|
width: countTxt.implicitWidth + 10
|
||||||
|
radius: 8
|
||||||
|
color: Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.20)
|
||||||
|
border.color: Theme.accent
|
||||||
|
border.width: 1
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: countTxt
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.previewWindowInstances ? root.previewWindowInstances.length + (root.previewWindowInstances.length === 1 ? " Window" : " Windows") : "0"
|
||||||
|
color: Theme.accent
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window Preview Card Row (Single or Multiple Instances)
|
||||||
|
RowLayout {
|
||||||
|
id: previewCardsRow
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.previewWindowInstances
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: cardItem
|
||||||
|
width: Math.max(140, Math.min(220, cardCaptionText.implicitWidth + 36))
|
||||||
|
height: 110
|
||||||
|
|
||||||
|
property bool cardHovered: cardMouseArea.containsMouse
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 10
|
||||||
|
color: cardItem.cardHovered ? Qt.rgba(255/255, 255/255, 255/255, 0.10) : Qt.rgba(255/255, 255/255, 255/255, 0.05)
|
||||||
|
border.color: modelData.active ? Theme.accent : (cardItem.cardHovered ? Theme.subAccent : Theme.separator)
|
||||||
|
border.width: modelData.active || cardItem.cardHovered ? 1.5 : 1
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
Behavior on border.color { ColorAnimation { duration: 120 } }
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
// Card Header: Window Caption & Close Button
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 6
|
||||||
|
height: 6
|
||||||
|
radius: 3
|
||||||
|
color: modelData.active ? Theme.accent : Theme.comment
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: cardCaptionText
|
||||||
|
text: modelData.caption || modelData.name || "Window"
|
||||||
|
color: modelData.active ? Theme.accent : Theme.fg
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.weight: modelData.active ? Font.Bold : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual Window Close Button (✕)
|
||||||
|
Item {
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
visible: cardItem.cardHovered
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 8
|
||||||
|
color: closeMouse.containsMouse ? Qt.rgba(255/255, 85/255, 85/255, 0.35) : "transparent"
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "✕"
|
||||||
|
color: closeMouse.containsMouse ? "#ff5555" : Theme.comment
|
||||||
|
font.pixelSize: 9
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: closeMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
TaskService.closeApp(modelData.appId)
|
||||||
|
previewPopIn.running = false
|
||||||
|
previewPopOut.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miniature Glass Window Preview Container
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: 6
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.25)
|
||||||
|
border.color: Theme.separator
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
// Mock Window Header Bar
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
height: 14
|
||||||
|
radius: 6
|
||||||
|
color: Qt.rgba(255/255, 255/255, 255/255, 0.06)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 6
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 4
|
||||||
|
Rectangle { width: 5; height: 5; radius: 2.5; color: "#ff5555" }
|
||||||
|
Rectangle { width: 5; height: 5; radius: 2.5; color: "#f1fa8c" }
|
||||||
|
Rectangle { width: 5; height: 5; radius: 2.5; color: "#50fa7b" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Icon Centerpiece inside Preview Box
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
anchors.verticalCenterOffset: 4
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
sourceSize.width: 32
|
||||||
|
sourceSize.height: 32
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
opacity: cardItem.cardHovered ? 1.0 : 0.82
|
||||||
|
source: root.getIconPath(modelData.icon || root.previewTargetApp.icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse Area to Focus Specific Window
|
||||||
|
MouseArea {
|
||||||
|
id: cardMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
let globalPos = cardItem.mapToItem(null, 0, 0)
|
||||||
|
TaskService.focusSpecificWindow(modelData, globalPos.x, globalPos.y)
|
||||||
|
previewPopIn.running = false
|
||||||
|
previewPopOut.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dock Context Menu Detached Popup Window
|
// Dock Context Menu Detached Popup Window
|
||||||
PopupWindow {
|
PopupWindow {
|
||||||
id: dockContextMenu
|
id: dockContextMenu
|
||||||
|
|||||||
@ -123,6 +123,52 @@ Item {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWindowsForApp(appId) {
|
||||||
|
if (!appId) return [];
|
||||||
|
let lower = appId.toLowerCase().trim()
|
||||||
|
let matching = []
|
||||||
|
|
||||||
|
for (let i = 0; i < openWindowApps.length; i++) {
|
||||||
|
let winObj = openWindowApps[i]
|
||||||
|
if (!winObj) continue;
|
||||||
|
let winApp = typeof winObj === "string" ? winObj.toLowerCase().trim() : (winObj.appId || "").toLowerCase().trim()
|
||||||
|
|
||||||
|
let isMatch = false
|
||||||
|
if (winApp === lower) {
|
||||||
|
isMatch = true
|
||||||
|
} else if (!winApp.startsWith("steam_app_") && !lower.startsWith("steam_app_") && lower !== "steam") {
|
||||||
|
if (winApp.includes(lower) || lower.includes(winApp)) {
|
||||||
|
isMatch = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
matching.push({
|
||||||
|
id: typeof winObj === "object" ? (winObj.id || winApp) : winApp,
|
||||||
|
appId: winApp,
|
||||||
|
name: typeof winObj === "object" ? (winObj.name || winApp) : winApp,
|
||||||
|
icon: typeof winObj === "object" ? (winObj.icon || winApp) : winApp,
|
||||||
|
caption: typeof winObj === "object" ? (winObj.caption || winObj.name || winApp) : winApp,
|
||||||
|
minimized: typeof winObj === "object" ? (winObj.minimized || false) : false,
|
||||||
|
active: typeof winObj === "object" ? (winObj.active || false) : false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matching
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusSpecificWindow(winObj, iconX, iconY) {
|
||||||
|
if (!winObj) return;
|
||||||
|
let query = (winObj.caption || winObj.appId || "").toLowerCase().trim()
|
||||||
|
let ix = Math.round(iconX || 0)
|
||||||
|
let iy = Math.round(iconY || 0)
|
||||||
|
|
||||||
|
root.activeAppId = (winObj.appId || "").toLowerCase().trim()
|
||||||
|
proc.running = false
|
||||||
|
proc.command = ["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/kwin_focus.py", query, ix.toString(), iy.toString()]
|
||||||
|
proc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
function focusApp(appId, cmd, iconX, iconY) {
|
function focusApp(appId, cmd, iconX, iconY) {
|
||||||
if (!appId) return;
|
if (!appId) return;
|
||||||
let lower = appId.toLowerCase().trim()
|
let lower = appId.toLowerCase().trim()
|
||||||
|
|||||||
@ -29,9 +29,14 @@ function updateWindows() {
|
|||||||
var isSystemShell = (cls === "quickshell" || cls === "plasmashell" || cls === "krunner" ||
|
var isSystemShell = (cls === "quickshell" || cls === "plasmashell" || cls === "krunner" ||
|
||||||
cls.indexOf("status_icon") !== -1 || cls.indexOf("tray") !== -1 || cls.indexOf("desktop") !== -1);
|
cls.indexOf("status_icon") !== -1 || cls.indexOf("tray") !== -1 || cls.indexOf("desktop") !== -1);
|
||||||
if (!isSystemShell) {
|
if (!isSystemShell) {
|
||||||
if (openApps.indexOf(cls) === -1) {
|
var winId = String(w.internalId || w.windowId || (cls + "_" + i));
|
||||||
openApps.push(cls);
|
openApps.push({
|
||||||
}
|
id: winId,
|
||||||
|
appId: cls,
|
||||||
|
caption: w.caption || cls,
|
||||||
|
minimized: w.minimized || false,
|
||||||
|
active: (w === activeWin || w.active || false)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -340,8 +340,21 @@ class ActiveAppService(dbus.service.Object):
|
|||||||
|
|
||||||
raw_wins = json.loads(open_windows_json)
|
raw_wins = json.loads(open_windows_json)
|
||||||
enriched_wins = []
|
enriched_wins = []
|
||||||
for app_id in raw_wins:
|
for item in raw_wins:
|
||||||
enriched_wins.append(resolve_app_info(app_id))
|
if isinstance(item, dict):
|
||||||
|
app_id = item.get("appId", "")
|
||||||
|
info = resolve_app_info(app_id)
|
||||||
|
enriched_wins.append({
|
||||||
|
"id": item.get("id", app_id),
|
||||||
|
"appId": app_id,
|
||||||
|
"name": info.get("name", app_id),
|
||||||
|
"icon": info.get("icon", app_id),
|
||||||
|
"caption": item.get("caption", info.get("name", app_id)),
|
||||||
|
"minimized": item.get("minimized", False),
|
||||||
|
"active": item.get("active", False)
|
||||||
|
})
|
||||||
|
elif isinstance(item, str):
|
||||||
|
enriched_wins.append(resolve_app_info(item))
|
||||||
|
|
||||||
with open(OPEN_WINS_FILE, "w") as f:
|
with open(OPEN_WINS_FILE, "w") as f:
|
||||||
f.write(json.dumps(enriched_wins))
|
f.write(json.dumps(enriched_wins))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user