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.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.pager 2.0
|
||||
import "DesktopLogic.js" as Logic
|
||||
|
||||
Item {
|
||||
id: root
|
||||
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 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 {
|
||||
id: pagerModel
|
||||
enabled: root.visible
|
||||
enabled: true
|
||||
showDesktop: false
|
||||
pagerType: PagerModel.VirtualDesktops
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// HELPER FUNCTIONS
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Store desktop IDs fetched from D-Bus
|
||||
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() {
|
||||
if (pagerModel.count > 0 && pagerModel.currentPage >= 0) {
|
||||
var idx = pagerModel.index(pagerModel.currentPage, 0)
|
||||
var name = pagerModel.data(idx, Qt.DisplayRole)
|
||||
return name || (t.desktop + " " + (pagerModel.currentPage + 1))
|
||||
}
|
||||
return t.desktop
|
||||
if (pagerModel.count > 0 && pagerModel.currentPage >= 0)
|
||||
return getDesktopName(pagerModel.currentPage)
|
||||
return "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 {
|
||||
id: compactArea
|
||||
Layout.minimumWidth: compactLabel.implicitWidth + 16
|
||||
hoverEnabled: true
|
||||
|
||||
|
|
@ -105,39 +111,127 @@ Item {
|
|||
onEntered: { hoverCompact = true; closeTimer.stop(); openTimer.start() }
|
||||
onExited: { hoverCompact = false; openTimer.stop(); if (!hoverPopup) closeTimer.start() }
|
||||
onClicked: { openTimer.stop(); closeTimer.stop(); plasmoid.expanded = !plasmoid.expanded }
|
||||
onWheel: {
|
||||
var next = wheel.angleDelta.y > 0
|
||||
? (pagerModel.currentPage - 1 + pagerModel.count) % pagerModel.count
|
||||
: (pagerModel.currentPage + 1) % pagerModel.count
|
||||
pagerModel.changePage(next)
|
||||
onWheel: function(wheel) {
|
||||
pagerModel.changePage(Logic.nextDesktop(pagerModel.currentPage, pagerModel.count, wheel.angleDelta.y > 0 ? -1 : 1))
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// FULL REPRESENTATION
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
Plasmoid.fullRepresentation: Item {
|
||||
id: popup
|
||||
|
||||
readonly property int cols: Math.max(1, Math.ceil(Math.sqrt(pagerModel.count)))
|
||||
readonly property int rows: Math.max(1, Math.ceil(pagerModel.count / cols))
|
||||
readonly property var gridDims: Logic.calculateGrid(pagerModel.count)
|
||||
readonly property real deskW: previewSize
|
||||
readonly property real deskH: pagerModel.pagerItemSize.height > 0
|
||||
? deskW * pagerModel.pagerItemSize.height / pagerModel.pagerItemSize.width
|
||||
: deskW * 9 / 16
|
||||
readonly property real deskH: Logic.calculatePreviewHeight(previewSize, pagerModel.pagerItemSize.width, pagerModel.pagerItemSize.height)
|
||||
readonly property real scaleX: deskW / Math.max(1, pagerModel.pagerItemSize.width)
|
||||
readonly property real scaleY: deskH / Math.max(1, pagerModel.pagerItemSize.height)
|
||||
|
||||
Layout.preferredWidth: cols * (deskW + 8) + 32
|
||||
Layout.preferredHeight: rows * (deskH + 8) + 70
|
||||
Layout.preferredWidth: gridDims.cols * (deskW + 8) + 32
|
||||
Layout.preferredHeight: gridDims.rows * (deskH + 8) + 70
|
||||
Layout.minimumWidth: 200
|
||||
Layout.minimumHeight: 120
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: { hoverPopup = true; closeTimer.stop() }
|
||||
onExited: { hoverPopup = false; if (!hoverCompact) closeTimer.start() }
|
||||
property int dragSource: -1
|
||||
property int dropTarget: -1
|
||||
|
||||
Timer {
|
||||
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 {
|
||||
|
|
@ -145,15 +239,12 @@ Item {
|
|||
anchors.margins: 10
|
||||
spacing: 8
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// DESKTOP GRID
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
Grid {
|
||||
id: desktopGrid
|
||||
id: grid
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
columns: popup.cols
|
||||
columns: popup.gridDims.cols
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
|
|
@ -161,70 +252,24 @@ Item {
|
|||
model: pagerModel
|
||||
|
||||
Rectangle {
|
||||
id: desktop
|
||||
id: desktopItem
|
||||
width: popup.deskW
|
||||
height: popup.deskH
|
||||
|
||||
readonly property string desktopName: model.display || (t.desktop + " " + (index + 1))
|
||||
readonly property bool isActive: index === pagerModel.currentPage
|
||||
property bool isHovered: false
|
||||
|
||||
color: isActive ? Qt.darker(PlasmaCore.Theme.highlightColor, 1.3) : PlasmaCore.Theme.backgroundColor
|
||||
border.width: isActive ? 2 : 1
|
||||
border.color: isActive ? PlasmaCore.Theme.highlightColor : PlasmaCore.Theme.disabledTextColor
|
||||
color: index === pagerModel.currentPage
|
||||
? Qt.darker(PlasmaCore.Theme.highlightColor, 1.3)
|
||||
: PlasmaCore.Theme.backgroundColor
|
||||
border.width: popup.dropTarget === index && popup.dragSource !== index ? 3 : (index === pagerModel.currentPage ? 2 : 1)
|
||||
border.color: popup.dropTarget === index && popup.dragSource !== index
|
||||
? "#3498db"
|
||||
: (index === pagerModel.currentPage ? PlasmaCore.Theme.highlightColor : PlasmaCore.Theme.disabledTextColor)
|
||||
radius: 4
|
||||
clip: true
|
||||
opacity: isHovered ? 1 : 0.92
|
||||
opacity: popup.dragSource === index ? 0.5 : (desktopMA.containsMouse || deleteMA.containsMouse ? 1 : 0.92)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
property string desktopName: model.display || ("Desktop " + (index + 1))
|
||||
property bool isHovered: desktopMA.containsMouse || deleteMA.containsMouse
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
// 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
|
||||
// ─────────────────────────────────────────────────
|
||||
// Windows
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
|
|
@ -233,17 +278,13 @@ Item {
|
|||
|
||||
Repeater {
|
||||
model: TasksModel
|
||||
|
||||
Rectangle {
|
||||
readonly property rect geo: model.Geometry
|
||||
readonly property bool minimized: model.IsMinimized === true
|
||||
|
||||
property rect geo: model.Geometry
|
||||
x: Math.round(geo.x * popup.scaleX)
|
||||
y: Math.round(geo.y * popup.scaleY)
|
||||
width: Math.max(8, Math.round(geo.width * popup.scaleX))
|
||||
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)
|
||||
border.width: 1
|
||||
border.color: model.IsActive ? PlasmaCore.Theme.highlightColor : PlasmaCore.Theme.textColor
|
||||
|
|
@ -261,181 +302,169 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
// 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
|
||||
// Badge
|
||||
Rectangle {
|
||||
visible: 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
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottomMargin: 3
|
||||
width: badgeLabel.implicitWidth + 12
|
||||
height: badgeLabel.implicitHeight + 4
|
||||
color: Qt.rgba(0,0,0,0.7)
|
||||
radius: 3
|
||||
|
||||
PlasmaComponents.Label {
|
||||
id: badgeLbl; anchors.centerIn: parent
|
||||
text: (index + 1) + " " + desktop.desktopName
|
||||
font.pixelSize: 11; font.bold: true; color: "white"
|
||||
id: badgeLabel
|
||||
anchors.centerIn: parent
|
||||
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 {
|
||||
id: resizeMouseArea
|
||||
id: desktopMA
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.SizeFDiagCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
property real startX: 0
|
||||
property real startY: 0
|
||||
property int startSize: 0
|
||||
property bool dragging: false
|
||||
|
||||
onPressed: {
|
||||
startX = mouse.x
|
||||
startY = mouse.y
|
||||
startSize = previewSize
|
||||
onPressed: function(mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
startX = mouse.x
|
||||
startY = mouse.y
|
||||
dragging = false
|
||||
}
|
||||
}
|
||||
onPositionChanged: {
|
||||
if (pressed) {
|
||||
var delta = (mouse.x - startX + mouse.y - startY) / 2
|
||||
var newSize = Math.max(80, Math.min(200, startSize + delta))
|
||||
plasmoid.configuration.previewSize = Math.round(newSize)
|
||||
|
||||
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
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// CONTEXT MENU
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
Menu {
|
||||
id: ctxMenu
|
||||
property int desktopIndex: 0
|
||||
property string desktopName: ""
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
spacing: 8
|
||||
|
||||
MenuItem {
|
||||
text: t.switchTo + " \"" + ctxMenu.desktopName + "\""
|
||||
icon.name: "go-jump"
|
||||
onTriggered: {
|
||||
pagerModel.changePage(ctxMenu.desktopIndex)
|
||||
plasmoid.expanded = false
|
||||
PlasmaComponents.Button {
|
||||
icon.name: "list-add"
|
||||
text: "Add"
|
||||
onClicked: run(Logic.buildCreateCommand(pagerModel.count, "Desktop " + (pagerModel.count + 1)))
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// RENAME DIALOG
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
Dialog {
|
||||
id: renameDlg
|
||||
property int idx: 0
|
||||
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()
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
PlasmaComponents.Label {
|
||||
text: pagerModel.count + " desktops"
|
||||
opacity: 0.6
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaCore.DataSource {
|
||||
id: execSource
|
||||
engine: "executable"
|
||||
onNewData: disconnectSource(sourceName)
|
||||
// Drag rectangle
|
||||
Rectangle {
|
||||
id: dragRect
|
||||
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