refactor: modularize main.qml into components, dialogs and JS modules
Split monolithic main.qml into separate files for better maintainability: - CompactRepresentation.qml, FullRepresentation.qml - DesktopDelegate.qml, Translations.qml - DesktopLogic.js, DesktopManager.js - components/DragRect.qml - dialogs/RenameDialog.qml, NewDesktopDialog.qml - Added unit tests for JS modules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4d13665893
commit
1823a3c4eb
|
|
@ -0,0 +1,44 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||||
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
|
import "." as Local
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var pagerModel
|
||||||
|
property string currentName: ""
|
||||||
|
|
||||||
|
signal requestOpen()
|
||||||
|
signal requestClose()
|
||||||
|
signal requestToggle()
|
||||||
|
signal wheelUp()
|
||||||
|
signal wheelDown()
|
||||||
|
|
||||||
|
Layout.minimumWidth: label.implicitWidth + 16
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: parent.containsMouse ? PlasmaCore.Theme.highlightColor : "transparent"
|
||||||
|
opacity: 0.2
|
||||||
|
radius: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
id: label
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: currentName || Local.Translations.t.desktop
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntered: requestOpen()
|
||||||
|
onExited: requestClose()
|
||||||
|
onClicked: requestToggle()
|
||||||
|
|
||||||
|
onWheel: function(wheel) {
|
||||||
|
if (wheel.angleDelta.y > 0) wheelUp()
|
||||||
|
else wheelDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||||
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
|
import org.kde.plasma.private.pager 2.0
|
||||||
|
import "." as Local
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int desktopIndex: 0
|
||||||
|
property string desktopName: ""
|
||||||
|
property bool isActive: false
|
||||||
|
property bool showPreviews: true
|
||||||
|
property bool showIcons: true
|
||||||
|
property real scaleX: 1
|
||||||
|
property real scaleY: 1
|
||||||
|
property bool isDragTarget: false
|
||||||
|
property bool isDragSource: false
|
||||||
|
property bool canDelete: true
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
signal rightClicked()
|
||||||
|
signal deleteRequested()
|
||||||
|
signal dragStarted(int index, string name)
|
||||||
|
signal dragMoved(real mouseX, real mouseY)
|
||||||
|
signal dragEnded()
|
||||||
|
|
||||||
|
property bool isHovered: mouseArea.containsMouse || deleteBtn.containsMouse
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: desktop
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.isDragTarget ? 0 : 2
|
||||||
|
color: root.isActive ? Qt.darker(PlasmaCore.Theme.highlightColor, 1.3) : PlasmaCore.Theme.backgroundColor
|
||||||
|
border.width: root.isDragTarget ? 3 : (root.isActive ? 2 : 1)
|
||||||
|
border.color: root.isDragTarget ? "#3498db" : (root.isActive ? PlasmaCore.Theme.highlightColor : PlasmaCore.Theme.disabledTextColor)
|
||||||
|
radius: 4; clip: true
|
||||||
|
opacity: root.isDragSource ? 0.5 : (root.isHovered ? 1 : 0.92)
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||||
|
Behavior on anchors.margins { NumberAnimation { duration: 100 } }
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent; anchors.margins: 2; clip: true
|
||||||
|
visible: root.showPreviews
|
||||||
|
Repeater {
|
||||||
|
model: TasksModel
|
||||||
|
Rectangle {
|
||||||
|
readonly property rect geo: model.Geometry
|
||||||
|
x: Math.round(geo.x * root.scaleX); y: Math.round(geo.y * root.scaleY)
|
||||||
|
width: Math.max(8, Math.round(geo.width * root.scaleX))
|
||||||
|
height: Math.max(6, Math.round(geo.height * root.scaleY))
|
||||||
|
visible: model.IsMinimized !== true
|
||||||
|
color: model.IsActive ? Qt.rgba(1,1,1,0.4) : Qt.rgba(1,1,1,0.2)
|
||||||
|
border.width: 1; border.color: model.IsActive ? PlasmaCore.Theme.highlightColor : PlasmaCore.Theme.textColor; radius: 2
|
||||||
|
PlasmaCore.IconItem {
|
||||||
|
visible: root.showIcons && parent.width > 16 && parent.height > 12
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Math.min(parent.width - 4, parent.height - 4, 20); height: width
|
||||||
|
source: model.decoration || "application-x-executable"; usesPlasmaTheme: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label (when no previews)
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent; visible: !root.showPreviews
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: root.desktopIndex + 1; font.bold: true; font.pixelSize: 18
|
||||||
|
color: root.isActive ? PlasmaCore.Theme.highlightedTextColor : PlasmaCore.Theme.textColor
|
||||||
|
}
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: root.desktopName; font.pixelSize: 10; width: root.width - 14
|
||||||
|
elide: Text.ElideRight; horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: root.isActive ? PlasmaCore.Theme.highlightedTextColor : PlasmaCore.Theme.textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badge (when previews enabled)
|
||||||
|
Rectangle {
|
||||||
|
visible: root.showPreviews
|
||||||
|
anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 3 }
|
||||||
|
width: badgeLbl.implicitWidth + 12; height: badgeLbl.implicitHeight + 4
|
||||||
|
color: Qt.rgba(0,0,0,0.7); radius: 3
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
id: badgeLbl; anchors.centerIn: parent
|
||||||
|
text: (root.desktopIndex + 1) + " " + root.desktopName
|
||||||
|
font.pixelSize: 11; font.bold: true; color: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
Rectangle {
|
||||||
|
id: deleteBtn
|
||||||
|
visible: root.isHovered && root.canDelete && !root.isDragSource
|
||||||
|
anchors { top: parent.top; right: parent.right; margins: 4 }
|
||||||
|
width: Math.min(parent.width * 0.25, 36); height: width; radius: width / 2
|
||||||
|
color: deleteArea.containsMouse ? "#e74c3c" : Qt.rgba(0,0,0,0.7)
|
||||||
|
property bool containsMouse: deleteArea.containsMouse
|
||||||
|
Behavior on color { ColorAnimation { duration: 100 } }
|
||||||
|
PlasmaCore.IconItem { anchors.centerIn: parent; width: parent.width * 0.6; height: width; source: "edit-delete" }
|
||||||
|
MouseArea { id: deleteArea; anchors.fill: parent; hoverEnabled: true; onClicked: root.deleteRequested() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main mouse area
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent; hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
property real startX: 0; property real startY: 0; property bool dragging: false
|
||||||
|
|
||||||
|
onPressed: function(mouse) { if (mouse.button === Qt.LeftButton) { startX = mouse.x; startY = mouse.y; dragging = false } }
|
||||||
|
onPositionChanged: function(mouse) {
|
||||||
|
if (!(mouse.buttons & Qt.LeftButton)) return
|
||||||
|
var dist = Math.sqrt(Math.pow(mouse.x - startX, 2) + Math.pow(mouse.y - startY, 2))
|
||||||
|
if (!dragging && dist > 10) { dragging = true; root.dragStarted(root.desktopIndex, root.desktopName) }
|
||||||
|
if (dragging) root.dragMoved(mouse.x, mouse.y)
|
||||||
|
}
|
||||||
|
onReleased: function(mouse) { if (mouse.button === Qt.LeftButton && dragging) root.dragEnded(); dragging = false }
|
||||||
|
onClicked: function(mouse) {
|
||||||
|
if (mouse.button === Qt.RightButton) root.rightClicked()
|
||||||
|
else if (!dragging) root.clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
// DesktopLogic.js - Pure business logic (testable with Node.js)
|
||||||
|
// All functions are pure - no side effects, same input = same output
|
||||||
|
.pragma library
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// COMMAND BUILDERS - Generate shell commands for KWin D-Bus operations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build command to remove a desktop
|
||||||
|
* @param {string} desktopId - The desktop UUID
|
||||||
|
* @returns {string|null} - Command string or null if invalid
|
||||||
|
*/
|
||||||
|
function buildRemoveCommand(desktopId) {
|
||||||
|
if (!desktopId || typeof desktopId !== 'string' || desktopId.trim() === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return "qdbus org.kde.KWin /VirtualDesktopManager removeDesktop '" + desktopId + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build command to rename a desktop
|
||||||
|
* @param {string} desktopId - The desktop UUID
|
||||||
|
* @param {string} newName - The new name
|
||||||
|
* @returns {string|null} - Command string or null if invalid
|
||||||
|
*/
|
||||||
|
function buildRenameCommand(desktopId, newName) {
|
||||||
|
if (!desktopId || typeof desktopId !== 'string' || desktopId.trim() === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!newName || typeof newName !== 'string' || newName.trim() === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
var escaped = escapeShell(newName.trim())
|
||||||
|
return "qdbus org.kde.KWin /VirtualDesktopManager setDesktopName '" + desktopId + "' '" + escaped + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build command to create a new desktop
|
||||||
|
* @param {number} position - Position index for new desktop
|
||||||
|
* @param {string} name - Name for new desktop
|
||||||
|
* @returns {string} - Command string
|
||||||
|
*/
|
||||||
|
function buildCreateCommand(position, name) {
|
||||||
|
var pos = Math.max(0, Math.floor(position))
|
||||||
|
var escaped = escapeShell(name ? name.trim() : 'Desktop')
|
||||||
|
return "qdbus org.kde.KWin /VirtualDesktopManager createDesktop " + pos + " '" + escaped + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build bash command to swap windows between two desktops using wmctrl
|
||||||
|
* @param {number} indexA - First desktop index (0-based)
|
||||||
|
* @param {number} indexB - Second desktop index (0-based)
|
||||||
|
* @returns {string} - Bash command string
|
||||||
|
*/
|
||||||
|
function buildSwapWindowsCommand(indexA, indexB) {
|
||||||
|
var a = Math.floor(indexA)
|
||||||
|
var b = Math.floor(indexB)
|
||||||
|
return "bash -c 'wins_a=$(wmctrl -l | awk \"\\$2==" + a + " {print \\$1}\"); " +
|
||||||
|
"wins_b=$(wmctrl -l | awk \"\\$2==" + b + " {print \\$1}\"); " +
|
||||||
|
"for w in $wins_a; do wmctrl -i -r $w -t " + b + "; done; " +
|
||||||
|
"for w in $wins_b; do wmctrl -i -r $w -t " + a + "; done'"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STRING UTILITIES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape string for safe use in shell single quotes
|
||||||
|
* @param {string} str - Input string
|
||||||
|
* @returns {string} - Escaped string
|
||||||
|
*/
|
||||||
|
function escapeShell(str) {
|
||||||
|
if (!str || typeof str !== 'string') return ''
|
||||||
|
return str.replace(/'/g, "'\\''")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GRID CALCULATIONS - Pure math functions for layout
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate optimal grid dimensions for N items
|
||||||
|
* Uses square root to create balanced grid
|
||||||
|
* @param {number} count - Number of items
|
||||||
|
* @returns {{cols: number, rows: number}} - Grid dimensions
|
||||||
|
*/
|
||||||
|
function calculateGrid(count) {
|
||||||
|
var n = Math.max(1, Math.floor(count))
|
||||||
|
var cols = Math.max(1, Math.ceil(Math.sqrt(n)))
|
||||||
|
var rows = Math.max(1, Math.ceil(n / cols))
|
||||||
|
return { cols: cols, rows: rows }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate preview height maintaining aspect ratio
|
||||||
|
* @param {number} width - Preview width
|
||||||
|
* @param {number} screenWidth - Screen width
|
||||||
|
* @param {number} screenHeight - Screen height
|
||||||
|
* @returns {number} - Calculated height
|
||||||
|
*/
|
||||||
|
function calculatePreviewHeight(width, screenWidth, screenHeight) {
|
||||||
|
if (!screenWidth || screenWidth <= 0 || !screenHeight || screenHeight <= 0) {
|
||||||
|
return width * 9 / 16 // Default 16:9
|
||||||
|
}
|
||||||
|
return width * screenHeight / screenWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate scale factors for positioning windows in preview
|
||||||
|
* @param {number} previewWidth - Preview width
|
||||||
|
* @param {number} previewHeight - Preview height
|
||||||
|
* @param {number} screenWidth - Screen width
|
||||||
|
* @param {number} screenHeight - Screen height
|
||||||
|
* @returns {{x: number, y: number}} - Scale factors
|
||||||
|
*/
|
||||||
|
function calculateScale(previewWidth, previewHeight, screenWidth, screenHeight) {
|
||||||
|
return {
|
||||||
|
x: previewWidth / Math.max(1, screenWidth),
|
||||||
|
y: previewHeight / Math.max(1, screenHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// NAVIGATION - Desktop switching logic
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate next desktop index with wrap-around
|
||||||
|
* @param {number} current - Current desktop index
|
||||||
|
* @param {number} count - Total number of desktops
|
||||||
|
* @param {number} direction - Direction: positive = forward, negative = backward
|
||||||
|
* @returns {number} - Next desktop index
|
||||||
|
*/
|
||||||
|
function nextDesktop(current, count, direction) {
|
||||||
|
if (count <= 0) return 0
|
||||||
|
var c = Math.max(0, Math.floor(current))
|
||||||
|
var n = Math.max(1, Math.floor(count))
|
||||||
|
|
||||||
|
if (direction > 0) {
|
||||||
|
return (c + 1) % n
|
||||||
|
} else {
|
||||||
|
return (c - 1 + n) % n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DRAG AND DROP - Validation and detection
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if swap operation is valid
|
||||||
|
* @param {number} indexA - Source index
|
||||||
|
* @param {number} indexB - Target index
|
||||||
|
* @param {number} count - Total count
|
||||||
|
* @returns {boolean} - True if swap is valid
|
||||||
|
*/
|
||||||
|
function canSwap(indexA, indexB, count) {
|
||||||
|
if (typeof indexA !== 'number' || typeof indexB !== 'number' || typeof count !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return indexA >= 0 && indexB >= 0 &&
|
||||||
|
indexA < count && indexB < count &&
|
||||||
|
indexA !== indexB
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find drop target index from mouse position
|
||||||
|
* This function needs QML objects, returns -1 for pure JS testing
|
||||||
|
* @param {object} repeater - QML Repeater
|
||||||
|
* @param {object} grid - QML Grid
|
||||||
|
* @param {number} mouseX - Mouse X in grid coordinates
|
||||||
|
* @param {number} mouseY - Mouse Y in grid coordinates
|
||||||
|
* @returns {number} - Target index or -1
|
||||||
|
*/
|
||||||
|
function findDropTarget(repeater, grid, mouseX, mouseY) {
|
||||||
|
if (!repeater || !grid || typeof repeater.count !== 'number') {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < repeater.count; i++) {
|
||||||
|
var item = repeater.itemAt(i)
|
||||||
|
if (!item) continue
|
||||||
|
|
||||||
|
var itemPos = grid.mapFromItem(item, 0, 0)
|
||||||
|
if (mouseX >= itemPos.x && mouseX < itemPos.x + item.width &&
|
||||||
|
mouseY >= itemPos.y && mouseY < itemPos.y + item.height) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate distance between two points
|
||||||
|
* @param {number} x1 - First point X
|
||||||
|
* @param {number} y1 - First point Y
|
||||||
|
* @param {number} x2 - Second point X
|
||||||
|
* @param {number} y2 - Second point Y
|
||||||
|
* @returns {number} - Distance
|
||||||
|
*/
|
||||||
|
function distance(x1, y1, x2, y2) {
|
||||||
|
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if drag threshold is exceeded
|
||||||
|
* @param {number} startX - Start X
|
||||||
|
* @param {number} startY - Start Y
|
||||||
|
* @param {number} currentX - Current X
|
||||||
|
* @param {number} currentY - Current Y
|
||||||
|
* @param {number} threshold - Drag threshold (default 10)
|
||||||
|
* @returns {boolean} - True if threshold exceeded
|
||||||
|
*/
|
||||||
|
function isDragStarted(startX, startY, currentX, currentY, threshold) {
|
||||||
|
var t = threshold || 10
|
||||||
|
return distance(startX, startY, currentX, currentY) > t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// VALIDATION - Input validation helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate desktop index
|
||||||
|
* @param {number} index - Index to validate
|
||||||
|
* @param {number} count - Total count
|
||||||
|
* @returns {boolean} - True if valid
|
||||||
|
*/
|
||||||
|
function isValidIndex(index, count) {
|
||||||
|
return typeof index === 'number' &&
|
||||||
|
typeof count === 'number' &&
|
||||||
|
index >= 0 &&
|
||||||
|
index < count &&
|
||||||
|
Number.isInteger(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate desktop name
|
||||||
|
* @param {string} name - Name to validate
|
||||||
|
* @returns {boolean} - True if valid
|
||||||
|
*/
|
||||||
|
function isValidName(name) {
|
||||||
|
return typeof name === 'string' && name.trim().length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamp value between min and max
|
||||||
|
* @param {number} value - Value to clamp
|
||||||
|
* @param {number} min - Minimum
|
||||||
|
* @param {number} max - Maximum
|
||||||
|
* @returns {number} - Clamped value
|
||||||
|
*/
|
||||||
|
function clamp(value, min, max) {
|
||||||
|
return Math.max(min, Math.min(max, value))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
// DesktopManager.js - Pure logic functions (testeable)
|
||||||
|
.pragma library
|
||||||
|
|
||||||
|
// Build command to remove a desktop
|
||||||
|
function buildRemoveCommand(desktopId) {
|
||||||
|
if (!desktopId) return null
|
||||||
|
return "qdbus org.kde.KWin /VirtualDesktopManager removeDesktop '" + desktopId + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build command to rename a desktop
|
||||||
|
function buildRenameCommand(desktopId, newName) {
|
||||||
|
if (!desktopId || !newName || !newName.trim()) return null
|
||||||
|
var escaped = newName.trim().replace(/'/g, "'\\''")
|
||||||
|
return "qdbus org.kde.KWin /VirtualDesktopManager setDesktopName '" + desktopId + "' '" + escaped + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build command to create a desktop
|
||||||
|
function buildCreateCommand(position, name) {
|
||||||
|
var escaped = name.trim().replace(/'/g, "'\\''")
|
||||||
|
return "qdbus org.kde.KWin /VirtualDesktopManager createDesktop " + position + " '" + escaped + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build commands to swap windows between desktops
|
||||||
|
function buildSwapWindowsCommand(indexA, indexB) {
|
||||||
|
return "bash -c 'wins_a=$(wmctrl -l | awk \"\\$2==" + indexA + " {print \\$1}\"); " +
|
||||||
|
"wins_b=$(wmctrl -l | awk \"\\$2==" + indexB + " {print \\$1}\"); " +
|
||||||
|
"for w in $wins_a; do wmctrl -i -r $w -t " + indexB + "; done; " +
|
||||||
|
"for w in $wins_b; do wmctrl -i -r $w -t " + indexA + "; done'"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape string for shell
|
||||||
|
function escapeShell(str) {
|
||||||
|
return str.replace(/'/g, "'\\''")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate grid dimensions
|
||||||
|
function calculateGrid(count) {
|
||||||
|
var cols = Math.max(1, Math.ceil(Math.sqrt(count)))
|
||||||
|
var rows = Math.max(1, Math.ceil(count / cols))
|
||||||
|
return { cols: cols, rows: rows }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate desktop preview dimensions
|
||||||
|
function calculatePreviewSize(baseSize, screenWidth, screenHeight) {
|
||||||
|
var width = baseSize
|
||||||
|
var height = screenHeight > 0 ? baseSize * screenHeight / screenWidth : baseSize * 9 / 16
|
||||||
|
return { width: width, height: height }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate scale factors for window positioning
|
||||||
|
function calculateScale(previewWidth, previewHeight, screenWidth, screenHeight) {
|
||||||
|
return {
|
||||||
|
x: previewWidth / Math.max(1, screenWidth),
|
||||||
|
y: previewHeight / Math.max(1, screenHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate swap operation
|
||||||
|
function canSwap(indexA, indexB, count) {
|
||||||
|
return indexA >= 0 && indexB >= 0 &&
|
||||||
|
indexA < count && indexB < count &&
|
||||||
|
indexA !== indexB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate next desktop (for wheel navigation)
|
||||||
|
function nextDesktop(current, count, direction) {
|
||||||
|
if (direction > 0) {
|
||||||
|
return (current + 1) % count
|
||||||
|
} else {
|
||||||
|
return (current - 1 + count) % count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||||
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
|
import "DesktopManager.js" as DM
|
||||||
|
import "components" as Components
|
||||||
|
import "dialogs" as Dialogs
|
||||||
|
import "." as Local
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var pagerModel
|
||||||
|
property bool showPreviews: true
|
||||||
|
property bool showIcons: true
|
||||||
|
property int previewSize: 130
|
||||||
|
|
||||||
|
signal hoverEntered()
|
||||||
|
signal hoverExited()
|
||||||
|
signal desktopClicked(int index)
|
||||||
|
signal deleteDesktop(int index)
|
||||||
|
signal createDesktop(string name)
|
||||||
|
signal renameDesktop(int index, string name)
|
||||||
|
signal swapDesktops(int indexA, int indexB)
|
||||||
|
signal sizeCommitted(int size)
|
||||||
|
signal closeRequested()
|
||||||
|
|
||||||
|
property bool dialogOpen: newDlg.visible || renameDlg.visible
|
||||||
|
|
||||||
|
// Use JS for calculations
|
||||||
|
readonly property var gridSize: DM.calculateGrid(pagerModel ? pagerModel.count : 1)
|
||||||
|
readonly property var previewDims: DM.calculatePreviewSize(
|
||||||
|
previewSize,
|
||||||
|
pagerModel ? pagerModel.pagerItemSize.width : 1920,
|
||||||
|
pagerModel ? pagerModel.pagerItemSize.height : 1080
|
||||||
|
)
|
||||||
|
readonly property var scaleFactor: DM.calculateScale(
|
||||||
|
previewDims.width, previewDims.height,
|
||||||
|
pagerModel ? pagerModel.pagerItemSize.width : 1920,
|
||||||
|
pagerModel ? pagerModel.pagerItemSize.height : 1080
|
||||||
|
)
|
||||||
|
|
||||||
|
Layout.preferredWidth: gridSize.cols * (previewDims.width + 8) + 32
|
||||||
|
Layout.preferredHeight: gridSize.rows * (previewDims.height + 8) + 70
|
||||||
|
Layout.minimumWidth: 200
|
||||||
|
Layout.minimumHeight: 120
|
||||||
|
|
||||||
|
// Drag state
|
||||||
|
property int dragSource: -1
|
||||||
|
property int dropTarget: -1
|
||||||
|
|
||||||
|
// Background mouse area
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
z: -1
|
||||||
|
onEntered: root.hoverEntered()
|
||||||
|
onExited: root.hoverExited()
|
||||||
|
onReleased: {
|
||||||
|
dragSource = -1
|
||||||
|
dropTarget = -1
|
||||||
|
dragRect.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
// Desktop grid
|
||||||
|
Grid {
|
||||||
|
id: grid
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
columns: gridSize.cols
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: repeater
|
||||||
|
model: pagerModel
|
||||||
|
|
||||||
|
DesktopDelegate {
|
||||||
|
width: previewDims.width
|
||||||
|
height: previewDims.height
|
||||||
|
desktopIndex: index
|
||||||
|
desktopName: model.display || (Local.Translations.t.desktop + " " + (index + 1))
|
||||||
|
isActive: pagerModel ? index === pagerModel.currentPage : false
|
||||||
|
showPreviews: root.showPreviews
|
||||||
|
showIcons: root.showIcons
|
||||||
|
scaleX: scaleFactor.x
|
||||||
|
scaleY: scaleFactor.y
|
||||||
|
isDragTarget: root.dropTarget === index && root.dragSource !== index
|
||||||
|
isDragSource: root.dragSource === index
|
||||||
|
canDelete: pagerModel ? pagerModel.count > 1 : false
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.desktopClicked(index)
|
||||||
|
root.closeRequested()
|
||||||
|
}
|
||||||
|
onRightClicked: ctxMenu.show(index, desktopName)
|
||||||
|
onDeleteRequested: root.deleteDesktop(index)
|
||||||
|
|
||||||
|
onDragStarted: function(idx, name) {
|
||||||
|
root.dragSource = idx
|
||||||
|
dragRect.dragName = name
|
||||||
|
dragRect.active = true
|
||||||
|
}
|
||||||
|
onDragMoved: function(mx, my) {
|
||||||
|
var pos = mapToItem(root, mx, my)
|
||||||
|
dragRect.x = pos.x - dragRect.width / 2
|
||||||
|
dragRect.y = pos.y - dragRect.height / 2
|
||||||
|
|
||||||
|
// Find drop target
|
||||||
|
var gpos = mapToItem(grid, mx, my)
|
||||||
|
root.dropTarget = -1
|
||||||
|
for (var i = 0; i < repeater.count; i++) {
|
||||||
|
var item = repeater.itemAt(i)
|
||||||
|
if (!item) continue
|
||||||
|
var ipos = grid.mapFromItem(item, 0, 0)
|
||||||
|
if (gpos.x >= ipos.x && gpos.x < ipos.x + item.width &&
|
||||||
|
gpos.y >= ipos.y && gpos.y < ipos.y + item.height) {
|
||||||
|
root.dropTarget = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDragEnded: {
|
||||||
|
if (root.dragSource >= 0 && root.dropTarget >= 0 && root.dragSource !== root.dropTarget) {
|
||||||
|
root.swapDesktops(root.dragSource, root.dropTarget)
|
||||||
|
}
|
||||||
|
root.dragSource = -1
|
||||||
|
root.dropTarget = -1
|
||||||
|
dragRect.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom bar
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
PlasmaComponents.Button {
|
||||||
|
icon.name: "list-add"
|
||||||
|
text: Local.Translations.t.add
|
||||||
|
onClicked: newDlg.open(Local.Translations.t.desktop + " " + (pagerModel ? pagerModel.count + 1 : 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
text: (pagerModel ? pagerModel.count : 0) + " " + Local.Translations.t.desktops
|
||||||
|
opacity: 0.6
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize handle
|
||||||
|
Rectangle {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
PlasmaCore.IconItem {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
source: "transform-scale"
|
||||||
|
opacity: resizeArea.containsMouse || resizeArea.pressed ? 1 : 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: resizeArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.SizeFDiagCursor
|
||||||
|
property int startY: 0
|
||||||
|
property int startSize: 0
|
||||||
|
|
||||||
|
onPressed: {
|
||||||
|
startY = mouseY
|
||||||
|
startSize = root.previewSize
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
if (pressed) {
|
||||||
|
root.previewSize = Math.max(80, Math.min(200, Math.round(startSize + (mouseY - startY) * 0.8)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: root.sizeCommitted(root.previewSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag rectangle
|
||||||
|
Components.DragRect {
|
||||||
|
id: dragRect
|
||||||
|
width: previewDims.width - 4
|
||||||
|
height: previewDims.height - 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialogs
|
||||||
|
Dialogs.NewDesktopDialog {
|
||||||
|
id: newDlg
|
||||||
|
onAccepted: function(name) { root.createDesktop(name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialogs.RenameDialog {
|
||||||
|
id: renameDlg
|
||||||
|
onAccepted: function(idx, name) { root.renameDesktop(idx, name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
Menu {
|
||||||
|
id: ctxMenu
|
||||||
|
property int idx: 0
|
||||||
|
property string name: ""
|
||||||
|
|
||||||
|
function show(i, n) {
|
||||||
|
idx = i
|
||||||
|
name = n
|
||||||
|
popup()
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: Local.Translations.t.switchTo + " \"" + ctxMenu.name + "\""
|
||||||
|
icon.name: "go-jump"
|
||||||
|
onTriggered: {
|
||||||
|
root.desktopClicked(ctxMenu.idx)
|
||||||
|
root.closeRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MenuSeparator {}
|
||||||
|
MenuItem {
|
||||||
|
text: Local.Translations.t.rename
|
||||||
|
icon.name: "edit-rename"
|
||||||
|
onTriggered: renameDlg.open(ctxMenu.idx, ctxMenu.name)
|
||||||
|
}
|
||||||
|
MenuItem {
|
||||||
|
text: Local.Translations.t.delete_
|
||||||
|
icon.name: "edit-delete"
|
||||||
|
enabled: pagerModel ? pagerModel.count > 1 : false
|
||||||
|
onTriggered: root.deleteDesktop(ctxMenu.idx)
|
||||||
|
}
|
||||||
|
MenuSeparator {}
|
||||||
|
MenuItem {
|
||||||
|
text: Local.Translations.t.newDesktop
|
||||||
|
icon.name: "list-add"
|
||||||
|
onTriggered: newDlg.open(Local.Translations.t.desktop + " " + (pagerModel ? pagerModel.count + 1 : 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
pragma Singleton
|
||||||
|
import QtQuick 2.15
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
readonly property string lang: Qt.locale().name.substring(0, 2)
|
||||||
|
|
||||||
|
readonly property var strings: ({
|
||||||
|
"en": {
|
||||||
|
desktop: "Desktop", add: "Add", desktops: "desktops",
|
||||||
|
rename: "Rename...", delete_: "Delete",
|
||||||
|
renameTitle: "Rename Desktop", switchTo: "Switch to",
|
||||||
|
newDesktop: "New Desktop", enterName: "Enter name:",
|
||||||
|
create: "Create", cancel: "Cancel", save: "Save"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
desktop: "Escritorio", add: "Agregar", desktops: "escritorios",
|
||||||
|
rename: "Renombrar...", delete_: "Eliminar",
|
||||||
|
renameTitle: "Renombrar Escritorio", switchTo: "Cambiar a",
|
||||||
|
newDesktop: "Nuevo Escritorio", enterName: "Ingrese nombre:",
|
||||||
|
create: "Crear", cancel: "Cancelar", save: "Guardar"
|
||||||
|
},
|
||||||
|
"zh": {
|
||||||
|
desktop: "桌面", add: "添加", desktops: "个桌面",
|
||||||
|
rename: "重命名...", delete_: "删除",
|
||||||
|
renameTitle: "重命名桌面", switchTo: "切换到",
|
||||||
|
newDesktop: "新建桌面", enterName: "输入名称:",
|
||||||
|
create: "创建", cancel: "取消", save: "保存"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
desktop: "Bureau", add: "Ajouter", desktops: "bureaux",
|
||||||
|
rename: "Renommer...", delete_: "Supprimer",
|
||||||
|
renameTitle: "Renommer le bureau", switchTo: "Basculer vers",
|
||||||
|
newDesktop: "Nouveau bureau", enterName: "Entrez le nom:",
|
||||||
|
create: "Créer", cancel: "Annuler", save: "Enregistrer"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
desktop: "Desktop", add: "Hinzufügen", desktops: "Desktops",
|
||||||
|
rename: "Umbenennen...", delete_: "Löschen",
|
||||||
|
renameTitle: "Desktop umbenennen", switchTo: "Wechseln zu",
|
||||||
|
newDesktop: "Neuer Desktop", enterName: "Name eingeben:",
|
||||||
|
create: "Erstellen", cancel: "Abbrechen", save: "Speichern"
|
||||||
|
},
|
||||||
|
"pt": {
|
||||||
|
desktop: "Área de trabalho", add: "Adicionar", desktops: "áreas",
|
||||||
|
rename: "Renomear...", delete_: "Excluir",
|
||||||
|
renameTitle: "Renomear", switchTo: "Mudar para",
|
||||||
|
newDesktop: "Nova área", enterName: "Digite o nome:",
|
||||||
|
create: "Criar", cancel: "Cancelar", save: "Salvar"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
readonly property var t: strings[lang] || strings["en"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||||
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string dragName: ""
|
||||||
|
property bool active: false
|
||||||
|
|
||||||
|
visible: active
|
||||||
|
color: PlasmaCore.Theme.highlightColor
|
||||||
|
opacity: 0.9
|
||||||
|
radius: 4
|
||||||
|
z: 1000
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.dragName
|
||||||
|
font.bold: true
|
||||||
|
color: PlasmaCore.Theme.highlightedTextColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||||
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
|
import ".." as Local
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
visible: false
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.7)
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
property string defaultName: ""
|
||||||
|
signal accepted(string name)
|
||||||
|
signal cancelled()
|
||||||
|
|
||||||
|
function open(name) {
|
||||||
|
defaultName = name
|
||||||
|
nameField.text = name
|
||||||
|
visible = true
|
||||||
|
nameField.forceActiveFocus()
|
||||||
|
nameField.selectAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible = false
|
||||||
|
cancelled()
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea { anchors.fill: parent; onClicked: root.close() }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 280; height: 130
|
||||||
|
color: PlasmaCore.Theme.backgroundColor
|
||||||
|
border.color: PlasmaCore.Theme.highlightColor
|
||||||
|
border.width: 1; radius: 8
|
||||||
|
|
||||||
|
MouseArea { anchors.fill: parent }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent; anchors.margins: 16; spacing: 12
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
text: Local.Translations.t.newDesktop
|
||||||
|
font.bold: true; font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents.TextField {
|
||||||
|
id: nameField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: Local.Translations.t.enterName
|
||||||
|
onAccepted: if (text.trim()) { root.accepted(text); root.close() }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right; spacing: 8
|
||||||
|
PlasmaComponents.Button {
|
||||||
|
text: Local.Translations.t.cancel
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
PlasmaComponents.Button {
|
||||||
|
text: Local.Translations.t.create
|
||||||
|
highlighted: true
|
||||||
|
onClicked: if (nameField.text.trim()) { root.accepted(nameField.text); root.close() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||||
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
|
import ".." as Local
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
visible: false
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.7)
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
property int desktopIndex: -1
|
||||||
|
signal accepted(int index, string name)
|
||||||
|
signal cancelled()
|
||||||
|
|
||||||
|
function open(index, name) {
|
||||||
|
desktopIndex = index
|
||||||
|
nameField.text = name
|
||||||
|
visible = true
|
||||||
|
nameField.forceActiveFocus()
|
||||||
|
nameField.selectAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible = false
|
||||||
|
cancelled()
|
||||||
|
}
|
||||||
|
|
||||||
|
function doRename() {
|
||||||
|
if (nameField.text.trim()) {
|
||||||
|
accepted(desktopIndex, nameField.text)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea { anchors.fill: parent; onClicked: root.close() }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 280; height: 130
|
||||||
|
color: PlasmaCore.Theme.backgroundColor
|
||||||
|
border.color: PlasmaCore.Theme.highlightColor
|
||||||
|
border.width: 1; radius: 8
|
||||||
|
|
||||||
|
MouseArea { anchors.fill: parent }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent; anchors.margins: 16; spacing: 12
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
text: Local.Translations.t.renameTitle
|
||||||
|
font.bold: true; font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents.TextField {
|
||||||
|
id: nameField
|
||||||
|
width: parent.width
|
||||||
|
onAccepted: root.doRename()
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right; spacing: 8
|
||||||
|
PlasmaComponents.Button {
|
||||||
|
text: Local.Translations.t.cancel
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
PlasmaComponents.Button {
|
||||||
|
text: Local.Translations.t.save
|
||||||
|
highlighted: true
|
||||||
|
onClicked: root.doRename()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,87 +5,93 @@ import org.kde.plasma.plasmoid 2.0
|
||||||
import org.kde.plasma.core 2.0 as PlasmaCore
|
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||||
import org.kde.plasma.private.pager 2.0
|
import org.kde.plasma.private.pager 2.0
|
||||||
|
import "DesktopLogic.js" as Logic
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation
|
Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// INTERNATIONALIZATION (i18n)
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
readonly property string systemLang: Qt.locale().name.substring(0, 2)
|
|
||||||
readonly property var translations: ({
|
|
||||||
"en": { desktop: "Desktop", add: "Add", desktops: "desktops", rename: "Rename...", delete_: "Delete",
|
|
||||||
renameTitle: "Rename Desktop", switchTo: "Switch to", moveWindowHere: "Move window here",
|
|
||||||
newDesktop: "New Desktop", confirmDelete: "Delete this desktop?" },
|
|
||||||
"es": { desktop: "Escritorio", add: "Agregar", desktops: "escritorios", rename: "Renombrar...", delete_: "Eliminar",
|
|
||||||
renameTitle: "Renombrar Escritorio", switchTo: "Cambiar a", moveWindowHere: "Mover ventana aquí",
|
|
||||||
newDesktop: "Nuevo Escritorio", confirmDelete: "¿Eliminar este escritorio?" },
|
|
||||||
"zh": { desktop: "桌面", add: "添加", desktops: "个桌面", rename: "重命名...", delete_: "删除",
|
|
||||||
renameTitle: "重命名桌面", switchTo: "切换到", moveWindowHere: "移动窗口到此处",
|
|
||||||
newDesktop: "新建桌面", confirmDelete: "删除此桌面?" },
|
|
||||||
"fr": { desktop: "Bureau", add: "Ajouter", desktops: "bureaux", rename: "Renommer...", delete_: "Supprimer",
|
|
||||||
renameTitle: "Renommer le bureau", switchTo: "Basculer vers", moveWindowHere: "Déplacer la fenêtre ici",
|
|
||||||
newDesktop: "Nouveau bureau", confirmDelete: "Supprimer ce bureau ?" },
|
|
||||||
"de": { desktop: "Desktop", add: "Hinzufügen", desktops: "Desktops", rename: "Umbenennen...", delete_: "Löschen",
|
|
||||||
renameTitle: "Desktop umbenennen", switchTo: "Wechseln zu", moveWindowHere: "Fenster hierher verschieben",
|
|
||||||
newDesktop: "Neuer Desktop", confirmDelete: "Diesen Desktop löschen?" },
|
|
||||||
"pt": { desktop: "Área de trabalho", add: "Adicionar", desktops: "áreas de trabalho", rename: "Renomear...", delete_: "Excluir",
|
|
||||||
renameTitle: "Renomear área de trabalho", switchTo: "Mudar para", moveWindowHere: "Mover janela para cá",
|
|
||||||
newDesktop: "Nova área de trabalho", confirmDelete: "Excluir esta área de trabalho?" }
|
|
||||||
})
|
|
||||||
readonly property var t: translations[systemLang] || translations["en"]
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// CONFIGURATION
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
readonly property bool showPreviews: plasmoid.configuration.showWindowPreviews
|
readonly property bool showPreviews: plasmoid.configuration.showWindowPreviews
|
||||||
readonly property bool showIcons: plasmoid.configuration.showWindowIcons
|
readonly property bool showIcons: plasmoid.configuration.showWindowIcons
|
||||||
readonly property int previewSize: plasmoid.configuration.previewSize || 130
|
property int previewSize: plasmoid.configuration.previewSize || 130
|
||||||
|
|
||||||
|
property bool hoverCompact: false
|
||||||
|
property bool hoverPopup: false
|
||||||
|
|
||||||
|
Timer { id: openTimer; interval: 80; onTriggered: plasmoid.expanded = true }
|
||||||
|
Timer { id: closeTimer; interval: 400; onTriggered: if (!hoverCompact && !hoverPopup) plasmoid.expanded = false }
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// PAGER MODEL (native KDE plugin with real window geometry)
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
PagerModel {
|
PagerModel {
|
||||||
id: pagerModel
|
id: pagerModel
|
||||||
enabled: root.visible
|
enabled: true
|
||||||
showDesktop: false
|
showDesktop: false
|
||||||
pagerType: PagerModel.VirtualDesktops
|
pagerType: PagerModel.VirtualDesktops
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// Store desktop IDs fetched from D-Bus
|
||||||
// HELPER FUNCTIONS
|
property var desktopIds: ({})
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
|
PlasmaCore.DataSource {
|
||||||
|
id: executable
|
||||||
|
engine: "executable"
|
||||||
|
onNewData: {
|
||||||
|
var stdout = data["stdout"] || ""
|
||||||
|
// Parse desktop IDs from qdbus output
|
||||||
|
if (sourceName.indexOf("desktops") > -1) {
|
||||||
|
parseDesktopIds(stdout)
|
||||||
|
}
|
||||||
|
disconnectSource(sourceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: refreshDesktopIds()
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshDesktopIds() {
|
||||||
|
executable.connectSource("qdbus --literal org.kde.KWin /VirtualDesktopManager org.kde.KWin.VirtualDesktopManager.desktops")
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDesktopIds(output) {
|
||||||
|
// Parse: [Argument: (uss) 0, "uuid", "name"], ...
|
||||||
|
var regex = /\[Argument: \(uss\) (\d+), "([^"]+)", "([^"]+)"\]/g
|
||||||
|
var match
|
||||||
|
var ids = {}
|
||||||
|
while ((match = regex.exec(output)) !== null) {
|
||||||
|
var idx = parseInt(match[1])
|
||||||
|
ids[idx] = match[2]
|
||||||
|
}
|
||||||
|
desktopIds = ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh IDs when desktop count changes
|
||||||
|
Connections {
|
||||||
|
target: pagerModel
|
||||||
|
function onCountChanged() {
|
||||||
|
refreshDesktopIds()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(cmd) {
|
||||||
|
if (cmd) {
|
||||||
|
executable.connectSource(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDesktopId(index) {
|
||||||
|
if (index < 0 || index >= pagerModel.count) return ""
|
||||||
|
return desktopIds[index] || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDesktopName(index) {
|
||||||
|
return pagerModel.data(pagerModel.index(index, 0), Qt.DisplayRole) || ("Desktop " + (index + 1))
|
||||||
|
}
|
||||||
|
|
||||||
function currentDesktopName() {
|
function currentDesktopName() {
|
||||||
if (pagerModel.count > 0 && pagerModel.currentPage >= 0) {
|
if (pagerModel.count > 0 && pagerModel.currentPage >= 0)
|
||||||
var idx = pagerModel.index(pagerModel.currentPage, 0)
|
return getDesktopName(pagerModel.currentPage)
|
||||||
var name = pagerModel.data(idx, Qt.DisplayRole)
|
return "Desktop"
|
||||||
return name || (t.desktop + " " + (pagerModel.currentPage + 1))
|
|
||||||
}
|
|
||||||
return t.desktop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDesktop() { pagerModel.addDesktop() }
|
|
||||||
function removeDesktop(index) {
|
|
||||||
if (pagerModel.count > 1) {
|
|
||||||
pagerModel.changePage(index)
|
|
||||||
pagerModel.removeDesktop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// HOVER TIMERS
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
property bool hoverCompact: false
|
|
||||||
property bool hoverPopup: false
|
|
||||||
Timer { id: openTimer; interval: 80; onTriggered: plasmoid.expanded = true }
|
|
||||||
Timer { id: closeTimer; interval: 300; onTriggered: if (!hoverCompact && !hoverPopup) plasmoid.expanded = false }
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// COMPACT REPRESENTATION
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
Plasmoid.compactRepresentation: MouseArea {
|
Plasmoid.compactRepresentation: MouseArea {
|
||||||
id: compactArea
|
|
||||||
Layout.minimumWidth: compactLabel.implicitWidth + 16
|
Layout.minimumWidth: compactLabel.implicitWidth + 16
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
||||||
|
|
@ -105,39 +111,127 @@ Item {
|
||||||
onEntered: { hoverCompact = true; closeTimer.stop(); openTimer.start() }
|
onEntered: { hoverCompact = true; closeTimer.stop(); openTimer.start() }
|
||||||
onExited: { hoverCompact = false; openTimer.stop(); if (!hoverPopup) closeTimer.start() }
|
onExited: { hoverCompact = false; openTimer.stop(); if (!hoverPopup) closeTimer.start() }
|
||||||
onClicked: { openTimer.stop(); closeTimer.stop(); plasmoid.expanded = !plasmoid.expanded }
|
onClicked: { openTimer.stop(); closeTimer.stop(); plasmoid.expanded = !plasmoid.expanded }
|
||||||
onWheel: {
|
onWheel: function(wheel) {
|
||||||
var next = wheel.angleDelta.y > 0
|
pagerModel.changePage(Logic.nextDesktop(pagerModel.currentPage, pagerModel.count, wheel.angleDelta.y > 0 ? -1 : 1))
|
||||||
? (pagerModel.currentPage - 1 + pagerModel.count) % pagerModel.count
|
|
||||||
: (pagerModel.currentPage + 1) % pagerModel.count
|
|
||||||
pagerModel.changePage(next)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// FULL REPRESENTATION
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
Plasmoid.fullRepresentation: Item {
|
Plasmoid.fullRepresentation: Item {
|
||||||
id: popup
|
id: popup
|
||||||
|
|
||||||
readonly property int cols: Math.max(1, Math.ceil(Math.sqrt(pagerModel.count)))
|
readonly property var gridDims: Logic.calculateGrid(pagerModel.count)
|
||||||
readonly property int rows: Math.max(1, Math.ceil(pagerModel.count / cols))
|
|
||||||
readonly property real deskW: previewSize
|
readonly property real deskW: previewSize
|
||||||
readonly property real deskH: pagerModel.pagerItemSize.height > 0
|
readonly property real deskH: Logic.calculatePreviewHeight(previewSize, pagerModel.pagerItemSize.width, pagerModel.pagerItemSize.height)
|
||||||
? deskW * pagerModel.pagerItemSize.height / pagerModel.pagerItemSize.width
|
|
||||||
: deskW * 9 / 16
|
|
||||||
readonly property real scaleX: deskW / Math.max(1, pagerModel.pagerItemSize.width)
|
readonly property real scaleX: deskW / Math.max(1, pagerModel.pagerItemSize.width)
|
||||||
readonly property real scaleY: deskH / Math.max(1, pagerModel.pagerItemSize.height)
|
readonly property real scaleY: deskH / Math.max(1, pagerModel.pagerItemSize.height)
|
||||||
|
|
||||||
Layout.preferredWidth: cols * (deskW + 8) + 32
|
Layout.preferredWidth: gridDims.cols * (deskW + 8) + 32
|
||||||
Layout.preferredHeight: rows * (deskH + 8) + 70
|
Layout.preferredHeight: gridDims.rows * (deskH + 8) + 70
|
||||||
Layout.minimumWidth: 200
|
Layout.minimumWidth: 200
|
||||||
Layout.minimumHeight: 120
|
Layout.minimumHeight: 120
|
||||||
|
|
||||||
MouseArea {
|
property int dragSource: -1
|
||||||
anchors.fill: parent
|
property int dropTarget: -1
|
||||||
hoverEnabled: true
|
|
||||||
onEntered: { hoverPopup = true; closeTimer.stop() }
|
Timer {
|
||||||
onExited: { hoverPopup = false; if (!hoverCompact) closeTimer.start() }
|
id: refreshTimer
|
||||||
|
interval: 300
|
||||||
|
onTriggered: pagerModel.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents.Menu {
|
||||||
|
id: contextMenu
|
||||||
|
property int desktopIndex: -1
|
||||||
|
property string desktopName: ""
|
||||||
|
property string desktopId: ""
|
||||||
|
|
||||||
|
PlasmaComponents.MenuItem {
|
||||||
|
text: "Switch to"
|
||||||
|
icon.name: "go-jump"
|
||||||
|
onClicked: {
|
||||||
|
pagerModel.changePage(contextMenu.desktopIndex)
|
||||||
|
plasmoid.expanded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlasmaComponents.MenuItem {
|
||||||
|
text: "Rename..."
|
||||||
|
icon.name: "edit-rename"
|
||||||
|
onClicked: {
|
||||||
|
renameDialog.desktopId = contextMenu.desktopId
|
||||||
|
renameDialog.desktopName = contextMenu.desktopName
|
||||||
|
renameDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlasmaComponents.MenuSeparator {}
|
||||||
|
PlasmaComponents.MenuItem {
|
||||||
|
text: "Delete"
|
||||||
|
icon.name: "edit-delete"
|
||||||
|
enabled: pagerModel.count > 1
|
||||||
|
onClicked: run(Logic.buildRemoveCommand(contextMenu.desktopId))
|
||||||
|
}
|
||||||
|
PlasmaComponents.MenuSeparator {}
|
||||||
|
PlasmaComponents.MenuItem {
|
||||||
|
text: "New Desktop"
|
||||||
|
icon.name: "list-add"
|
||||||
|
onClicked: run(Logic.buildCreateCommand(pagerModel.count, "Desktop " + (pagerModel.count + 1)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: renameDialog
|
||||||
|
title: "Rename Desktop"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
modal: true
|
||||||
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
|
||||||
|
property string desktopId: ""
|
||||||
|
property string desktopName: ""
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
renameField.text = desktopName
|
||||||
|
renameField.selectAll()
|
||||||
|
renameField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (renameField.text.trim()) {
|
||||||
|
run(Logic.buildRenameCommand(desktopId, renameField.text.trim()))
|
||||||
|
refreshTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 10
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
text: "Enter new name:"
|
||||||
|
}
|
||||||
|
PlasmaComponents.TextField {
|
||||||
|
id: renameField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredWidth: 250
|
||||||
|
onAccepted: renameDialog.accept()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track hover state for entire popup
|
||||||
|
HoverHandler {
|
||||||
|
id: popupHover
|
||||||
|
onHoveredChanged: {
|
||||||
|
if (hovered) {
|
||||||
|
hoverPopup = true
|
||||||
|
closeTimer.stop()
|
||||||
|
} else {
|
||||||
|
hoverPopup = false
|
||||||
|
if (!hoverCompact) closeTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup drag on release anywhere
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onCanceled: { popup.dragSource = -1; popup.dropTarget = -1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
@ -145,15 +239,12 @@ Item {
|
||||||
anchors.margins: 10
|
anchors.margins: 10
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// DESKTOP GRID
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
Grid {
|
Grid {
|
||||||
id: desktopGrid
|
id: grid
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
columns: popup.cols
|
columns: popup.gridDims.cols
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
|
|
@ -161,70 +252,24 @@ Item {
|
||||||
model: pagerModel
|
model: pagerModel
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: desktop
|
id: desktopItem
|
||||||
width: popup.deskW
|
width: popup.deskW
|
||||||
height: popup.deskH
|
height: popup.deskH
|
||||||
|
color: index === pagerModel.currentPage
|
||||||
readonly property string desktopName: model.display || (t.desktop + " " + (index + 1))
|
? Qt.darker(PlasmaCore.Theme.highlightColor, 1.3)
|
||||||
readonly property bool isActive: index === pagerModel.currentPage
|
: PlasmaCore.Theme.backgroundColor
|
||||||
property bool isHovered: false
|
border.width: popup.dropTarget === index && popup.dragSource !== index ? 3 : (index === pagerModel.currentPage ? 2 : 1)
|
||||||
|
border.color: popup.dropTarget === index && popup.dragSource !== index
|
||||||
color: isActive ? Qt.darker(PlasmaCore.Theme.highlightColor, 1.3) : PlasmaCore.Theme.backgroundColor
|
? "#3498db"
|
||||||
border.width: isActive ? 2 : 1
|
: (index === pagerModel.currentPage ? PlasmaCore.Theme.highlightColor : PlasmaCore.Theme.disabledTextColor)
|
||||||
border.color: isActive ? PlasmaCore.Theme.highlightColor : PlasmaCore.Theme.disabledTextColor
|
|
||||||
radius: 4
|
radius: 4
|
||||||
clip: true
|
clip: true
|
||||||
opacity: isHovered ? 1 : 0.92
|
opacity: popup.dragSource === index ? 0.5 : (desktopMA.containsMouse || deleteMA.containsMouse ? 1 : 0.92)
|
||||||
|
|
||||||
MouseArea {
|
property string desktopName: model.display || ("Desktop " + (index + 1))
|
||||||
anchors.fill: parent
|
property bool isHovered: desktopMA.containsMouse || deleteMA.containsMouse
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
onEntered: { hoverPopup = true; closeTimer.stop(); desktop.isHovered = true }
|
|
||||||
onExited: { desktop.isHovered = false }
|
|
||||||
onClicked: {
|
|
||||||
if (mouse.button === Qt.RightButton) {
|
|
||||||
ctxMenu.desktopIndex = index
|
|
||||||
ctxMenu.desktopName = desktop.desktopName
|
|
||||||
ctxMenu.popup()
|
|
||||||
} else {
|
|
||||||
pagerModel.changePage(index)
|
|
||||||
plasmoid.expanded = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────
|
// Windows
|
||||||
// DELETE BUTTON (top-right, on hover)
|
|
||||||
// ─────────────────────────────────────────────────
|
|
||||||
Rectangle {
|
|
||||||
id: deleteBtn
|
|
||||||
visible: desktop.isHovered && pagerModel.count > 1
|
|
||||||
anchors { top: parent.top; right: parent.right; margins: 2 }
|
|
||||||
width: Math.min(parent.width * 0.25, 44)
|
|
||||||
height: width
|
|
||||||
radius: width / 2
|
|
||||||
color: deleteMouseArea.containsMouse ? "#e74c3c" : Qt.rgba(0,0,0,0.6)
|
|
||||||
|
|
||||||
PlasmaCore.IconItem {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width * 0.6
|
|
||||||
height: width
|
|
||||||
source: "edit-delete"
|
|
||||||
usesPlasmaTheme: false
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: deleteMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: removeDesktop(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────
|
|
||||||
// WINDOWS with real geometry
|
|
||||||
// ─────────────────────────────────────────────────
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 2
|
anchors.margins: 2
|
||||||
|
|
@ -233,17 +278,13 @@ Item {
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: TasksModel
|
model: TasksModel
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
readonly property rect geo: model.Geometry
|
property rect geo: model.Geometry
|
||||||
readonly property bool minimized: model.IsMinimized === true
|
|
||||||
|
|
||||||
x: Math.round(geo.x * popup.scaleX)
|
x: Math.round(geo.x * popup.scaleX)
|
||||||
y: Math.round(geo.y * popup.scaleY)
|
y: Math.round(geo.y * popup.scaleY)
|
||||||
width: Math.max(8, Math.round(geo.width * popup.scaleX))
|
width: Math.max(8, Math.round(geo.width * popup.scaleX))
|
||||||
height: Math.max(6, Math.round(geo.height * popup.scaleY))
|
height: Math.max(6, Math.round(geo.height * popup.scaleY))
|
||||||
visible: !minimized
|
visible: model.IsMinimized !== true
|
||||||
|
|
||||||
color: model.IsActive ? Qt.rgba(1,1,1,0.4) : Qt.rgba(1,1,1,0.2)
|
color: model.IsActive ? Qt.rgba(1,1,1,0.4) : Qt.rgba(1,1,1,0.2)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: model.IsActive ? PlasmaCore.Theme.highlightColor : PlasmaCore.Theme.textColor
|
border.color: model.IsActive ? PlasmaCore.Theme.highlightColor : PlasmaCore.Theme.textColor
|
||||||
|
|
@ -261,181 +302,169 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────
|
// Badge
|
||||||
// DESKTOP LABEL (centered, when no previews)
|
|
||||||
// ─────────────────────────────────────────────────
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: !showPreviews
|
|
||||||
PlasmaComponents.Label {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: index + 1
|
|
||||||
font.bold: true; font.pixelSize: 18
|
|
||||||
color: desktop.isActive ? PlasmaCore.Theme.highlightedTextColor : PlasmaCore.Theme.textColor
|
|
||||||
}
|
|
||||||
PlasmaComponents.Label {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: desktop.desktopName
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: desktop.isActive ? PlasmaCore.Theme.highlightedTextColor : PlasmaCore.Theme.textColor
|
|
||||||
width: popup.deskW - 10
|
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Badge when previews enabled
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: showPreviews
|
anchors.bottom: parent.bottom
|
||||||
anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 3 }
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
width: badgeLbl.implicitWidth + 12; height: badgeLbl.implicitHeight + 4
|
anchors.bottomMargin: 3
|
||||||
color: Qt.rgba(0,0,0,0.7); radius: 3
|
width: badgeLabel.implicitWidth + 12
|
||||||
|
height: badgeLabel.implicitHeight + 4
|
||||||
|
color: Qt.rgba(0,0,0,0.7)
|
||||||
|
radius: 3
|
||||||
|
|
||||||
PlasmaComponents.Label {
|
PlasmaComponents.Label {
|
||||||
id: badgeLbl; anchors.centerIn: parent
|
id: badgeLabel
|
||||||
text: (index + 1) + " " + desktop.desktopName
|
anchors.centerIn: parent
|
||||||
font.pixelSize: 11; font.bold: true; color: "white"
|
text: (index + 1) + " " + desktopItem.desktopName
|
||||||
|
font.pixelSize: 11
|
||||||
|
font.bold: true
|
||||||
|
color: "white"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// BOTTOM BAR
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
MouseArea {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
hoverEnabled: true
|
|
||||||
onEntered: { hoverPopup = true; closeTimer.stop() }
|
|
||||||
onExited: { hoverPopup = false; if (!hoverCompact) closeTimer.start() }
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 8
|
|
||||||
PlasmaComponents.Button {
|
|
||||||
icon.name: "list-add"; text: t.add
|
|
||||||
onClicked: pagerModel.addDesktop()
|
|
||||||
}
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
PlasmaComponents.Label {
|
|
||||||
text: pagerModel.count + " " + t.desktops
|
|
||||||
opacity: 0.6; font.pixelSize: 11
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────
|
|
||||||
// RESIZE HANDLE
|
|
||||||
// ─────────────────────────────────────────────────
|
|
||||||
Rectangle {
|
|
||||||
width: 20; height: 20
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
PlasmaCore.IconItem {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: "transform-scale"
|
|
||||||
opacity: resizeMouseArea.containsMouse ? 1 : 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: resizeMouseArea
|
id: desktopMA
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.SizeFDiagCursor
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
|
||||||
property real startX: 0
|
property real startX: 0
|
||||||
property real startY: 0
|
property real startY: 0
|
||||||
property int startSize: 0
|
property bool dragging: false
|
||||||
|
|
||||||
onPressed: {
|
onPressed: function(mouse) {
|
||||||
startX = mouse.x
|
if (mouse.button === Qt.LeftButton) {
|
||||||
startY = mouse.y
|
startX = mouse.x
|
||||||
startSize = previewSize
|
startY = mouse.y
|
||||||
|
dragging = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onPositionChanged: {
|
|
||||||
if (pressed) {
|
onPositionChanged: function(mouse) {
|
||||||
var delta = (mouse.x - startX + mouse.y - startY) / 2
|
if (!(mouse.buttons & Qt.LeftButton)) return
|
||||||
var newSize = Math.max(80, Math.min(200, startSize + delta))
|
var dist = Math.sqrt(Math.pow(mouse.x - startX, 2) + Math.pow(mouse.y - startY, 2))
|
||||||
plasmoid.configuration.previewSize = Math.round(newSize)
|
if (!dragging && dist > 10) {
|
||||||
|
dragging = true
|
||||||
|
popup.dragSource = index
|
||||||
|
dragRect.dragName = desktopItem.desktopName
|
||||||
|
}
|
||||||
|
if (dragging) {
|
||||||
|
var pos = mapToItem(popup, mouse.x, mouse.y)
|
||||||
|
dragRect.x = pos.x - dragRect.width / 2
|
||||||
|
dragRect.y = pos.y - dragRect.height / 2
|
||||||
|
|
||||||
|
var gpos = mapToItem(grid, mouse.x, mouse.y)
|
||||||
|
popup.dropTarget = Logic.findDropTarget(repeater, grid, gpos.x, gpos.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleased: function(mouse) {
|
||||||
|
if (mouse.button === Qt.LeftButton && dragging && Logic.canSwap(popup.dragSource, popup.dropTarget, pagerModel.count)) {
|
||||||
|
var idxA = popup.dragSource
|
||||||
|
var idxB = popup.dropTarget
|
||||||
|
var nameA = getDesktopName(idxA)
|
||||||
|
var nameB = getDesktopName(idxB)
|
||||||
|
var idA = getDesktopId(idxA)
|
||||||
|
var idB = getDesktopId(idxB)
|
||||||
|
|
||||||
|
if (idA && idB) {
|
||||||
|
run(Logic.buildSwapWindowsCommand(idxA, idxB))
|
||||||
|
run(Logic.buildRenameCommand(idA, nameB))
|
||||||
|
run(Logic.buildRenameCommand(idB, nameA))
|
||||||
|
refreshTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dragging = false
|
||||||
|
popup.dragSource = -1
|
||||||
|
popup.dropTarget = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: function(mouse) {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
contextMenu.desktopIndex = index
|
||||||
|
contextMenu.desktopName = desktopItem.desktopName
|
||||||
|
contextMenu.desktopId = getDesktopId(index)
|
||||||
|
contextMenu.popup()
|
||||||
|
} else if (!dragging) {
|
||||||
|
pagerModel.changePage(index)
|
||||||
|
plasmoid.expanded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button - declared AFTER desktopMA to receive events first
|
||||||
|
Rectangle {
|
||||||
|
id: deleteBtn
|
||||||
|
visible: desktopItem.isHovered && pagerModel.count > 1 && popup.dragSource < 0
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: 4
|
||||||
|
width: Math.min(parent.width * 0.25, 36)
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
color: deleteMA.containsMouse ? "#e74c3c" : Qt.rgba(0,0,0,0.7)
|
||||||
|
|
||||||
|
PlasmaCore.IconItem {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width * 0.6
|
||||||
|
height: width
|
||||||
|
source: "edit-delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deleteMA
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
var id = getDesktopId(index)
|
||||||
|
if (id) run(Logic.buildRemoveCommand(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
RowLayout {
|
||||||
// CONTEXT MENU
|
Layout.fillWidth: true
|
||||||
// ─────────────────────────────────────────────────────────────────
|
Layout.preferredHeight: 36
|
||||||
Menu {
|
spacing: 8
|
||||||
id: ctxMenu
|
|
||||||
property int desktopIndex: 0
|
|
||||||
property string desktopName: ""
|
|
||||||
|
|
||||||
MenuItem {
|
PlasmaComponents.Button {
|
||||||
text: t.switchTo + " \"" + ctxMenu.desktopName + "\""
|
icon.name: "list-add"
|
||||||
icon.name: "go-jump"
|
text: "Add"
|
||||||
onTriggered: {
|
onClicked: run(Logic.buildCreateCommand(pagerModel.count, "Desktop " + (pagerModel.count + 1)))
|
||||||
pagerModel.changePage(ctxMenu.desktopIndex)
|
|
||||||
plasmoid.expanded = false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
MenuSeparator {}
|
|
||||||
MenuItem {
|
|
||||||
text: t.rename
|
|
||||||
icon.name: "edit-rename"
|
|
||||||
onTriggered: {
|
|
||||||
renameDlg.idx = ctxMenu.desktopIndex
|
|
||||||
renameDlg.open()
|
|
||||||
renameField.text = ctxMenu.desktopName
|
|
||||||
renameField.selectAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MenuItem {
|
|
||||||
text: t.delete_
|
|
||||||
icon.name: "edit-delete"
|
|
||||||
enabled: pagerModel.count > 1
|
|
||||||
onTriggered: removeDesktop(ctxMenu.desktopIndex)
|
|
||||||
}
|
|
||||||
MenuSeparator {}
|
|
||||||
MenuItem {
|
|
||||||
text: t.newDesktop
|
|
||||||
icon.name: "list-add"
|
|
||||||
onTriggered: pagerModel.addDesktop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
Item { Layout.fillWidth: true }
|
||||||
// RENAME DIALOG
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
PlasmaComponents.Label {
|
||||||
Dialog {
|
text: pagerModel.count + " desktops"
|
||||||
id: renameDlg
|
opacity: 0.6
|
||||||
property int idx: 0
|
font.pixelSize: 11
|
||||||
title: t.renameTitle
|
|
||||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
contentItem: PlasmaComponents.TextField {
|
|
||||||
id: renameField
|
|
||||||
Layout.preferredWidth: 180
|
|
||||||
onAccepted: renameDlg.accept()
|
|
||||||
}
|
|
||||||
onAccepted: {
|
|
||||||
if (renameField.text.trim()) {
|
|
||||||
var desktopItem = repeater.itemAt(idx)
|
|
||||||
if (desktopItem) {
|
|
||||||
execSource.connectSource("qdbus org.kde.KWin /VirtualDesktopManager setDesktopName '" +
|
|
||||||
pagerModel.data(pagerModel.index(idx, 0), 0x0100 + 1) + "' '" +
|
|
||||||
renameField.text.trim().replace(/'/g, "'\\''") + "'")
|
|
||||||
}
|
|
||||||
pagerModel.refresh()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PlasmaCore.DataSource {
|
// Drag rectangle
|
||||||
id: execSource
|
Rectangle {
|
||||||
engine: "executable"
|
id: dragRect
|
||||||
onNewData: disconnectSource(sourceName)
|
visible: popup.dragSource >= 0
|
||||||
|
width: popup.deskW - 4
|
||||||
|
height: popup.deskH - 4
|
||||||
|
color: PlasmaCore.Theme.highlightColor
|
||||||
|
opacity: 0.9
|
||||||
|
radius: 4
|
||||||
|
z: 1000
|
||||||
|
|
||||||
|
property string dragName: ""
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: dragRect.dragName
|
||||||
|
font.bold: true
|
||||||
|
color: PlasmaCore.Theme.highlightedTextColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
singleton Translations 1.0 Translations.qml
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Run all tests for Virtual Desktop Switcher
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ Virtual Desktop Switcher - Test Suite ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
echo "▶ Running test_DesktopLogic.js..."
|
||||||
|
if node test_DesktopLogic.js; then
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "▶ Running test_DesktopManager.js..."
|
||||||
|
if node test_DesktopManager.js; then
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ $FAILED -eq 0 ]; then
|
||||||
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ ✓ ALL TESTS PASSED ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ ✗ SOME TESTS FAILED ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,587 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Test Suite for DesktopLogic.js
|
||||||
|
* Provides comprehensive coverage of all pure functions
|
||||||
|
* Run: node test_DesktopLogic.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Load and prepare the module
|
||||||
|
const srcPath = path.join(__dirname, '../contents/ui/DesktopLogic.js');
|
||||||
|
let code = fs.readFileSync(srcPath, 'utf8');
|
||||||
|
code = code.replace('.pragma library', '');
|
||||||
|
code += `
|
||||||
|
module.exports = {
|
||||||
|
buildRemoveCommand, buildRenameCommand, buildCreateCommand, buildSwapWindowsCommand,
|
||||||
|
escapeShell, calculateGrid, calculatePreviewHeight, calculateScale,
|
||||||
|
nextDesktop, canSwap, findDropTarget, distance, isDragStarted,
|
||||||
|
isValidIndex, isValidName, clamp
|
||||||
|
};`;
|
||||||
|
const tmpPath = '/tmp/DesktopLogic_test.js';
|
||||||
|
fs.writeFileSync(tmpPath, code);
|
||||||
|
const Logic = require(tmpPath);
|
||||||
|
|
||||||
|
// Test utilities
|
||||||
|
let passed = 0, failed = 0, total = 0;
|
||||||
|
const results = { passed: [], failed: [] };
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
total++;
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
passed++;
|
||||||
|
results.passed.push(name);
|
||||||
|
console.log(` ✓ ${name}`);
|
||||||
|
} catch (e) {
|
||||||
|
failed++;
|
||||||
|
results.failed.push({ name, error: e.message });
|
||||||
|
console.log(` ✗ ${name}`);
|
||||||
|
console.log(` Error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertEqual(actual, expected, msg) {
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(`${msg || 'Assertion failed'}: expected "${expected}", got "${actual}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertDeepEqual(actual, expected, msg) {
|
||||||
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||||
|
throw new Error(`${msg || 'Assertion failed'}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertTrue(condition, msg) {
|
||||||
|
if (!condition) throw new Error(msg || 'Expected true');
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFalse(condition, msg) {
|
||||||
|
if (condition) throw new Error(msg || 'Expected false');
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNull(value, msg) {
|
||||||
|
if (value !== null) throw new Error(`${msg || 'Expected null'}, got ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNotNull(value, msg) {
|
||||||
|
if (value === null) throw new Error(msg || 'Expected non-null');
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertContains(str, substr, msg) {
|
||||||
|
if (!str.includes(substr)) {
|
||||||
|
throw new Error(`${msg || 'String does not contain'}: "${substr}" not in "${str}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertApprox(actual, expected, tolerance, msg) {
|
||||||
|
if (Math.abs(actual - expected) > tolerance) {
|
||||||
|
throw new Error(`${msg || 'Approximation failed'}: expected ~${expected}, got ${actual}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TEST SUITES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ DesktopLogic.js - Comprehensive Test Suite ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('┌─ buildRemoveCommand ─────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('returns valid command for valid ID', () => {
|
||||||
|
const result = Logic.buildRemoveCommand('abc-123');
|
||||||
|
assertEqual(result, "qdbus org.kde.KWin /VirtualDesktopManager removeDesktop 'abc-123'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for null ID', () => {
|
||||||
|
assertNull(Logic.buildRemoveCommand(null));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for undefined ID', () => {
|
||||||
|
assertNull(Logic.buildRemoveCommand(undefined));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for empty string ID', () => {
|
||||||
|
assertNull(Logic.buildRemoveCommand(''));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for whitespace-only ID', () => {
|
||||||
|
assertNull(Logic.buildRemoveCommand(' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for numeric ID', () => {
|
||||||
|
assertNull(Logic.buildRemoveCommand(123));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles UUID format', () => {
|
||||||
|
const result = Logic.buildRemoveCommand('550e8400-e29b-41d4-a716-446655440000');
|
||||||
|
assertContains(result, '550e8400-e29b-41d4-a716-446655440000');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ buildRenameCommand ─────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('returns valid command for valid inputs', () => {
|
||||||
|
const result = Logic.buildRenameCommand('id1', 'Work');
|
||||||
|
assertEqual(result, "qdbus org.kde.KWin /VirtualDesktopManager setDesktopName 'id1' 'Work'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapes single quotes in name', () => {
|
||||||
|
const result = Logic.buildRenameCommand('id1', "it's mine");
|
||||||
|
assertContains(result, "it'\\''s mine");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trims whitespace from name', () => {
|
||||||
|
const result = Logic.buildRenameCommand('id1', ' Work ');
|
||||||
|
assertContains(result, "'Work'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for null ID', () => {
|
||||||
|
assertNull(Logic.buildRenameCommand(null, 'name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for null name', () => {
|
||||||
|
assertNull(Logic.buildRenameCommand('id', null));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for empty name', () => {
|
||||||
|
assertNull(Logic.buildRenameCommand('id', ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for whitespace-only name', () => {
|
||||||
|
assertNull(Logic.buildRenameCommand('id', ' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles special characters in name', () => {
|
||||||
|
const result = Logic.buildRenameCommand('id', 'Dev & Test');
|
||||||
|
assertContains(result, 'Dev & Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles unicode in name', () => {
|
||||||
|
const result = Logic.buildRenameCommand('id', '桌面');
|
||||||
|
assertContains(result, '桌面');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ buildCreateCommand ───────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('returns valid command for position 0', () => {
|
||||||
|
const result = Logic.buildCreateCommand(0, 'New Desktop');
|
||||||
|
assertEqual(result, "qdbus org.kde.KWin /VirtualDesktopManager createDesktop 0 'New Desktop'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns valid command for position 5', () => {
|
||||||
|
const result = Logic.buildCreateCommand(5, 'Desktop 6');
|
||||||
|
assertContains(result, 'createDesktop 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles negative position (clamps to 0)', () => {
|
||||||
|
const result = Logic.buildCreateCommand(-1, 'Test');
|
||||||
|
assertContains(result, 'createDesktop 0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles float position (floors)', () => {
|
||||||
|
const result = Logic.buildCreateCommand(2.7, 'Test');
|
||||||
|
assertContains(result, 'createDesktop 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapes single quotes in name', () => {
|
||||||
|
const result = Logic.buildCreateCommand(0, "John's Desktop");
|
||||||
|
assertContains(result, "John'\\''s Desktop");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles null name (uses default)', () => {
|
||||||
|
const result = Logic.buildCreateCommand(0, null);
|
||||||
|
assertContains(result, "'Desktop'");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ buildSwapWindowsCommand ────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('returns bash command with wmctrl', () => {
|
||||||
|
const result = Logic.buildSwapWindowsCommand(0, 1);
|
||||||
|
assertContains(result, 'bash -c');
|
||||||
|
assertContains(result, 'wmctrl');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('includes both desktop indices', () => {
|
||||||
|
const result = Logic.buildSwapWindowsCommand(2, 5);
|
||||||
|
assertContains(result, '==2');
|
||||||
|
assertContains(result, '==5');
|
||||||
|
assertContains(result, '-t 5');
|
||||||
|
assertContains(result, '-t 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles desktop 0', () => {
|
||||||
|
const result = Logic.buildSwapWindowsCommand(0, 3);
|
||||||
|
assertContains(result, '==0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('floors float indices', () => {
|
||||||
|
const result = Logic.buildSwapWindowsCommand(1.9, 3.1);
|
||||||
|
assertContains(result, '==1');
|
||||||
|
assertContains(result, '==3');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ escapeShell ────────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('escapes single quote', () => {
|
||||||
|
assertEqual(Logic.escapeShell("it's"), "it'\\''s");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapes multiple single quotes', () => {
|
||||||
|
assertEqual(Logic.escapeShell("it's John's"), "it'\\''s John'\\''s");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty for null', () => {
|
||||||
|
assertEqual(Logic.escapeShell(null), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty for undefined', () => {
|
||||||
|
assertEqual(Logic.escapeShell(undefined), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty for number', () => {
|
||||||
|
assertEqual(Logic.escapeShell(123), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves string without quotes', () => {
|
||||||
|
assertEqual(Logic.escapeShell('hello world'), 'hello world');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ calculateGrid ──────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('1 item = 1x1 grid', () => {
|
||||||
|
assertDeepEqual(Logic.calculateGrid(1), { cols: 1, rows: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('2 items = 2x1 grid', () => {
|
||||||
|
assertDeepEqual(Logic.calculateGrid(2), { cols: 2, rows: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('3 items = 2x2 grid', () => {
|
||||||
|
assertDeepEqual(Logic.calculateGrid(3), { cols: 2, rows: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('4 items = 2x2 grid', () => {
|
||||||
|
assertDeepEqual(Logic.calculateGrid(4), { cols: 2, rows: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('5 items = 3x2 grid', () => {
|
||||||
|
assertDeepEqual(Logic.calculateGrid(5), { cols: 3, rows: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('9 items = 3x3 grid', () => {
|
||||||
|
assertDeepEqual(Logic.calculateGrid(9), { cols: 3, rows: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('16 items = 4x4 grid', () => {
|
||||||
|
assertDeepEqual(Logic.calculateGrid(16), { cols: 4, rows: 4 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('0 items = 1x1 grid (minimum)', () => {
|
||||||
|
assertDeepEqual(Logic.calculateGrid(0), { cols: 1, rows: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('negative items = 1x1 grid (minimum)', () => {
|
||||||
|
assertDeepEqual(Logic.calculateGrid(-5), { cols: 1, rows: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('18 items = 5x4 grid', () => {
|
||||||
|
const result = Logic.calculateGrid(18);
|
||||||
|
assertTrue(result.cols * result.rows >= 18);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ calculatePreviewHeight ─────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('maintains 16:9 aspect ratio', () => {
|
||||||
|
const height = Logic.calculatePreviewHeight(160, 1920, 1080);
|
||||||
|
assertApprox(height, 90, 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maintains 4:3 aspect ratio', () => {
|
||||||
|
const height = Logic.calculatePreviewHeight(120, 1024, 768);
|
||||||
|
assertApprox(height, 90, 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses 16:9 default for zero width', () => {
|
||||||
|
const height = Logic.calculatePreviewHeight(160, 0, 0);
|
||||||
|
assertApprox(height, 90, 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses 16:9 default for negative dimensions', () => {
|
||||||
|
const height = Logic.calculatePreviewHeight(160, -100, -100);
|
||||||
|
assertApprox(height, 90, 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles ultrawide (21:9)', () => {
|
||||||
|
const height = Logic.calculatePreviewHeight(210, 2560, 1080);
|
||||||
|
assertApprox(height, 88.59, 0.1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ calculateScale ─────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('calculates correct scale factors', () => {
|
||||||
|
const scale = Logic.calculateScale(130, 73, 1920, 1080);
|
||||||
|
assertApprox(scale.x, 130/1920, 0.0001);
|
||||||
|
assertApprox(scale.y, 73/1080, 0.0001);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles zero screen dimensions', () => {
|
||||||
|
const scale = Logic.calculateScale(130, 73, 0, 0);
|
||||||
|
assertEqual(scale.x, 130);
|
||||||
|
assertEqual(scale.y, 73);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ nextDesktop ────────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('forward from 0 to 1', () => {
|
||||||
|
assertEqual(Logic.nextDesktop(0, 5, 1), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forward from 4 wraps to 0', () => {
|
||||||
|
assertEqual(Logic.nextDesktop(4, 5, 1), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('backward from 0 wraps to 4', () => {
|
||||||
|
assertEqual(Logic.nextDesktop(0, 5, -1), 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('backward from 2 to 1', () => {
|
||||||
|
assertEqual(Logic.nextDesktop(2, 5, -1), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles count of 1', () => {
|
||||||
|
assertEqual(Logic.nextDesktop(0, 1, 1), 0);
|
||||||
|
assertEqual(Logic.nextDesktop(0, 1, -1), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles count of 0 (returns 0)', () => {
|
||||||
|
assertEqual(Logic.nextDesktop(0, 0, 1), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('floors float current', () => {
|
||||||
|
assertEqual(Logic.nextDesktop(1.9, 5, 1), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ canSwap ────────────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('valid swap returns true', () => {
|
||||||
|
assertTrue(Logic.canSwap(0, 1, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('same index returns false', () => {
|
||||||
|
assertFalse(Logic.canSwap(2, 2, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('negative indexA returns false', () => {
|
||||||
|
assertFalse(Logic.canSwap(-1, 1, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('negative indexB returns false', () => {
|
||||||
|
assertFalse(Logic.canSwap(0, -1, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('indexA >= count returns false', () => {
|
||||||
|
assertFalse(Logic.canSwap(5, 1, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('indexB >= count returns false', () => {
|
||||||
|
assertFalse(Logic.canSwap(0, 5, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-number indexA returns false', () => {
|
||||||
|
assertFalse(Logic.canSwap('0', 1, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-number indexB returns false', () => {
|
||||||
|
assertFalse(Logic.canSwap(0, '1', 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-number count returns false', () => {
|
||||||
|
assertFalse(Logic.canSwap(0, 1, '5'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('boundary valid: 0 and count-1', () => {
|
||||||
|
assertTrue(Logic.canSwap(0, 4, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ findDropTarget ─────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('returns -1 for null repeater', () => {
|
||||||
|
assertEqual(Logic.findDropTarget(null, {}, 0, 0), -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns -1 for null grid', () => {
|
||||||
|
assertEqual(Logic.findDropTarget({count: 1}, null, 0, 0), -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns -1 for invalid repeater', () => {
|
||||||
|
assertEqual(Logic.findDropTarget({}, {}, 0, 0), -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ distance ─────────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('distance from origin', () => {
|
||||||
|
assertEqual(Logic.distance(0, 0, 3, 4), 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('distance same point is 0', () => {
|
||||||
|
assertEqual(Logic.distance(5, 5, 5, 5), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('distance negative coordinates', () => {
|
||||||
|
assertEqual(Logic.distance(-3, 0, 0, 4), 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('distance horizontal', () => {
|
||||||
|
assertEqual(Logic.distance(0, 0, 10, 0), 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('distance vertical', () => {
|
||||||
|
assertEqual(Logic.distance(0, 0, 0, 10), 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ isDragStarted ────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('returns false below threshold', () => {
|
||||||
|
assertFalse(Logic.isDragStarted(0, 0, 5, 5, 10));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true above threshold', () => {
|
||||||
|
assertTrue(Logic.isDragStarted(0, 0, 15, 0, 10));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false at exact threshold', () => {
|
||||||
|
assertFalse(Logic.isDragStarted(0, 0, 10, 0, 10));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses default threshold of 10', () => {
|
||||||
|
assertFalse(Logic.isDragStarted(0, 0, 8, 0));
|
||||||
|
assertTrue(Logic.isDragStarted(0, 0, 12, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ isValidIndex ───────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('valid index returns true', () => {
|
||||||
|
assertTrue(Logic.isValidIndex(0, 5));
|
||||||
|
assertTrue(Logic.isValidIndex(4, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('negative index returns false', () => {
|
||||||
|
assertFalse(Logic.isValidIndex(-1, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('index >= count returns false', () => {
|
||||||
|
assertFalse(Logic.isValidIndex(5, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('float index returns false', () => {
|
||||||
|
assertFalse(Logic.isValidIndex(1.5, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('string index returns false', () => {
|
||||||
|
assertFalse(Logic.isValidIndex('0', 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ isValidName ────────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('valid name returns true', () => {
|
||||||
|
assertTrue(Logic.isValidName('Work'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty string returns false', () => {
|
||||||
|
assertFalse(Logic.isValidName(''));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('whitespace only returns false', () => {
|
||||||
|
assertFalse(Logic.isValidName(' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('null returns false', () => {
|
||||||
|
assertFalse(Logic.isValidName(null));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number returns false', () => {
|
||||||
|
assertFalse(Logic.isValidName(123));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('name with spaces returns true', () => {
|
||||||
|
assertTrue(Logic.isValidName('My Work'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
console.log('\n┌─ clamp ──────────────────────────────────────────────────────────');
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('value within range unchanged', () => {
|
||||||
|
assertEqual(Logic.clamp(50, 0, 100), 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('value below min clamped', () => {
|
||||||
|
assertEqual(Logic.clamp(-10, 0, 100), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('value above max clamped', () => {
|
||||||
|
assertEqual(Logic.clamp(150, 0, 100), 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('value at min unchanged', () => {
|
||||||
|
assertEqual(Logic.clamp(0, 0, 100), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('value at max unchanged', () => {
|
||||||
|
assertEqual(Logic.clamp(100, 0, 100), 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RESULTS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ TEST RESULTS ║');
|
||||||
|
console.log('╠════════════════════════════════════════════════════════════════╣');
|
||||||
|
console.log(`║ Total: ${String(total).padStart(3)} ║`);
|
||||||
|
console.log(`║ Passed: ${String(passed).padStart(3)} ✓ ║`);
|
||||||
|
console.log(`║ Failed: ${String(failed).padStart(3)} ${failed > 0 ? '✗' : ' '} ║`);
|
||||||
|
console.log(`║ Coverage: ${((passed/total)*100).toFixed(1)}% ║`);
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════════╝');
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
console.log('\n Failed tests:');
|
||||||
|
results.failed.forEach(f => console.log(` - ${f.name}: ${f.error}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(failed === 0 ? '\n✓ All tests passed!' : '\n✗ Some tests failed');
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// Test suite for DesktopManager.js
|
||||||
|
// Run with: node test_DesktopManager.js
|
||||||
|
|
||||||
|
// Load the module (remove QML-specific pragma)
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const srcPath = path.join(__dirname, '../contents/ui/DesktopManager.js');
|
||||||
|
let code = fs.readFileSync(srcPath, 'utf8');
|
||||||
|
code = code.replace('.pragma library', '');
|
||||||
|
code += '\nmodule.exports = { buildRemoveCommand, buildRenameCommand, buildCreateCommand, buildSwapWindowsCommand, escapeShell, calculateGrid, calculatePreviewSize, calculateScale, canSwap, nextDesktop };';
|
||||||
|
fs.writeFileSync('/tmp/DM_test.js', code);
|
||||||
|
const DM = require('/tmp/DM_test.js');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
function test(name, condition) {
|
||||||
|
if (condition) {
|
||||||
|
console.log(`✓ ${name}`);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
console.log(`✗ ${name}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testEqual(name, actual, expected) {
|
||||||
|
if (actual === expected) {
|
||||||
|
console.log(`✓ ${name}`);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
console.log(`✗ ${name}: expected "${expected}", got "${actual}"`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== Testing DesktopManager.js ===\n');
|
||||||
|
|
||||||
|
// Test buildRemoveCommand
|
||||||
|
console.log('--- buildRemoveCommand ---');
|
||||||
|
testEqual('Remove with valid ID',
|
||||||
|
DM.buildRemoveCommand('abc-123'),
|
||||||
|
"qdbus org.kde.KWin /VirtualDesktopManager removeDesktop 'abc-123'"
|
||||||
|
);
|
||||||
|
test('Remove with null ID returns null', DM.buildRemoveCommand(null) === null);
|
||||||
|
test('Remove with empty ID returns null', DM.buildRemoveCommand('') === null);
|
||||||
|
|
||||||
|
// Test buildRenameCommand
|
||||||
|
console.log('\n--- buildRenameCommand ---');
|
||||||
|
testEqual('Rename simple',
|
||||||
|
DM.buildRenameCommand('id1', 'Work'),
|
||||||
|
"qdbus org.kde.KWin /VirtualDesktopManager setDesktopName 'id1' 'Work'"
|
||||||
|
);
|
||||||
|
test('Rename with null ID returns null', DM.buildRenameCommand(null, 'name') === null);
|
||||||
|
test('Rename with empty name returns null', DM.buildRenameCommand('id', '') === null);
|
||||||
|
test('Rename with whitespace name returns null', DM.buildRenameCommand('id', ' ') === null);
|
||||||
|
|
||||||
|
// Test escapeShell
|
||||||
|
console.log('\n--- escapeShell ---');
|
||||||
|
testEqual('Escape single quote', DM.escapeShell("it's"), "it'\\''s");
|
||||||
|
testEqual('No escape needed', DM.escapeShell("hello"), "hello");
|
||||||
|
|
||||||
|
// Test buildCreateCommand
|
||||||
|
console.log('\n--- buildCreateCommand ---');
|
||||||
|
testEqual('Create at position 5',
|
||||||
|
DM.buildCreateCommand(5, 'New Desktop'),
|
||||||
|
"qdbus org.kde.KWin /VirtualDesktopManager createDesktop 5 'New Desktop'"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test buildSwapWindowsCommand
|
||||||
|
console.log('\n--- buildSwapWindowsCommand ---');
|
||||||
|
test('Swap command contains wmctrl',
|
||||||
|
DM.buildSwapWindowsCommand(0, 1).includes('wmctrl')
|
||||||
|
);
|
||||||
|
test('Swap command contains both indices',
|
||||||
|
DM.buildSwapWindowsCommand(2, 5).includes('2') && DM.buildSwapWindowsCommand(2, 5).includes('5')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test calculateGrid
|
||||||
|
console.log('\n--- calculateGrid ---');
|
||||||
|
let grid1 = DM.calculateGrid(1);
|
||||||
|
testEqual('Grid for 1: cols', grid1.cols, 1);
|
||||||
|
testEqual('Grid for 1: rows', grid1.rows, 1);
|
||||||
|
|
||||||
|
let grid4 = DM.calculateGrid(4);
|
||||||
|
testEqual('Grid for 4: cols', grid4.cols, 2);
|
||||||
|
testEqual('Grid for 4: rows', grid4.rows, 2);
|
||||||
|
|
||||||
|
let grid5 = DM.calculateGrid(5);
|
||||||
|
testEqual('Grid for 5: cols', grid5.cols, 3);
|
||||||
|
testEqual('Grid for 5: rows', grid5.rows, 2);
|
||||||
|
|
||||||
|
let grid9 = DM.calculateGrid(9);
|
||||||
|
testEqual('Grid for 9: cols', grid9.cols, 3);
|
||||||
|
testEqual('Grid for 9: rows', grid9.rows, 3);
|
||||||
|
|
||||||
|
// Test calculatePreviewSize
|
||||||
|
console.log('\n--- calculatePreviewSize ---');
|
||||||
|
let preview = DM.calculatePreviewSize(130, 1920, 1080);
|
||||||
|
testEqual('Preview width', preview.width, 130);
|
||||||
|
test('Preview height is proportional', Math.abs(preview.height - 73.125) < 0.01);
|
||||||
|
|
||||||
|
// Test calculateScale
|
||||||
|
console.log('\n--- calculateScale ---');
|
||||||
|
let scale = DM.calculateScale(130, 73, 1920, 1080);
|
||||||
|
test('Scale X is positive', scale.x > 0);
|
||||||
|
test('Scale Y is positive', scale.y > 0);
|
||||||
|
|
||||||
|
// Test canSwap
|
||||||
|
console.log('\n--- canSwap ---');
|
||||||
|
test('canSwap valid', DM.canSwap(0, 1, 5) === true);
|
||||||
|
test('canSwap same index', DM.canSwap(2, 2, 5) === false);
|
||||||
|
test('canSwap negative A', DM.canSwap(-1, 1, 5) === false);
|
||||||
|
test('canSwap negative B', DM.canSwap(0, -1, 5) === false);
|
||||||
|
test('canSwap A out of bounds', DM.canSwap(5, 1, 5) === false);
|
||||||
|
test('canSwap B out of bounds', DM.canSwap(0, 5, 5) === false);
|
||||||
|
|
||||||
|
// Test nextDesktop
|
||||||
|
console.log('\n--- nextDesktop ---');
|
||||||
|
testEqual('Next from 0 (forward)', DM.nextDesktop(0, 5, 1), 1);
|
||||||
|
testEqual('Next from 4 (forward, wrap)', DM.nextDesktop(4, 5, 1), 0);
|
||||||
|
testEqual('Next from 0 (backward, wrap)', DM.nextDesktop(0, 5, -1), 4);
|
||||||
|
testEqual('Next from 2 (backward)', DM.nextDesktop(2, 5, -1), 1);
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('\n=== Results ===');
|
||||||
|
console.log(`Passed: ${passed}`);
|
||||||
|
console.log(`Failed: ${failed}`);
|
||||||
|
console.log(failed === 0 ? '\n✓ All tests passed!' : '\n✗ Some tests failed');
|
||||||
|
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
Loading…
Reference in New Issue