shift-shell/tests/check-convergence-dock-invariant.sh
Marco Allegretti 2aae067337 Keep desktop pager buttons together in dock
Show all virtual desktop buttons as one compact group next to the Desktop button instead of splitting them across both dock edges. Update the convergence dock invariant so this layout does not regress.
2026-05-29 11:05:42 +02:00

299 lines
22 KiB
Bash

#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Marco Allegretti <mightymarco4@gmail.com>
# SPDX-License-Identifier: GPL-2.0-or-later
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
constants="$repo_root/components/mobileshell/qml/components/Constants.qml"
panel="$repo_root/containments/panel/qml/main.qml"
status_panel="$repo_root/containments/panel/qml/StatusPanel.qml"
status_bar_template="$repo_root/layout-templates/org.kde.plasma.mobile.defaultStatusBar/contents/layout.js"
taskpanel="$repo_root/containments/taskpanel/qml/main.qml"
folio_main="$repo_root/containments/homescreens/folio/qml/main.qml"
shellutil_header="$repo_root/components/mobileshell/shellutil.h"
shellutil_cpp="$repo_root/components/mobileshell/shellutil.cpp"
swipearea_header="$repo_root/components/mobileshell/components/swipearea.h"
swipearea_cpp="$repo_root/components/mobileshell/components/swipearea.cpp"
folio_home="$repo_root/containments/homescreens/folio/qml/FolioHomeScreen.qml"
favourites_bar="$repo_root/containments/homescreens/folio/qml/FavouritesBar.qml"
folio_backend="$repo_root/containments/homescreens/folio/homescreen.h"
folio_backend_cpp="$repo_root/containments/homescreens/folio/homescreen.cpp"
action_content="$repo_root/components/mobileshell/qml/actiondrawer/private/ContentContainer.qml"
action_open_surface="$repo_root/components/mobileshell/qml/actiondrawer/ActionDrawerOpenSurface.qml"
action_landscape="$repo_root/components/mobileshell/qml/actiondrawer/private/LandscapeContentContainer.qml"
quick_settings="$repo_root/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml"
quick_settings_panel="$repo_root/components/mobileshell/qml/actiondrawer/private/QuickSettingsPanel.qml"
base_item="$repo_root/components/mobileshell/qml/components/BaseItem.qml"
notification_popup_manager="$repo_root/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml"
notification_popup="$repo_root/components/mobileshell/qml/popups/notifications/NotificationPopup.qml"
notification_popup_item="$repo_root/components/mobileshell/qml/widgets/notifications/NotificationPopupItem.qml"
notification_card="$repo_root/components/mobileshell/qml/widgets/notifications/NotificationCard.qml"
notification_drawer="$repo_root/components/mobileshell/qml/actiondrawer/private/NotificationDrawer.qml"
notifications_widget="$repo_root/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml"
require_line() {
local file="$1"
local needle="$2"
if ! grep -Fq -- "$needle" "$file"; then
echo "Missing invariant in ${file#$repo_root/}: $needle" >&2
exit 1
fi
}
require_line "$constants" "readonly property real convergenceDockHeight: Kirigami.Units.gridUnit * 3"
require_line "$constants" "readonly property real convergenceDockRevealHeight: Kirigami.Units.gridUnit"
require_line "$constants" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$constants" "readonly property real convergenceWorkspaceFrameRadius:"
require_line "$panel" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$panel" "root.panel.location = PlasmaCore.Types.TopEdge"
require_line "$panel" "root.panel.offset = 0"
require_line "$panel" "root.panel.visibilityMode = (ShellSettings.Settings.convergenceModeEnabled || ShellSettings.Settings.autoHidePanelsEnabled) ? 3 : 0"
require_line "$panel" "readonly property real topBarHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight"
require_line "$panel" "readonly property real topBarInputHeight: topBarHeight + convergenceWorkspaceFrameThickness"
require_line "$panel" "? 0"
require_line "$panel" ": (ShellSettings.Settings.convergenceModeEnabled ? topBarInputHeight : MobileShell.Constants.topPanelHeight)"
require_line "$panel" "MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, root.panel.width, root.panel.height));"
require_line "$panel" "height: Math.max(1, root.topBarInputHeight)"
require_line "$panel" "LayerShell.Window.exclusionZone: Math.max(1, root.topBarInputHeight)"
require_line "$panel" "visible: !ShellSettings.Settings.gamingModeEnabled"
require_line "$status_panel" "&& !ShellSettings.Settings.convergenceModeEnabled"
require_line "$status_panel" "visible: !ShellSettings.Settings.convergenceModeEnabled"
require_line "$status_panel" "TaskManager.VirtualDesktopInfo {"
require_line "$status_panel" "MobileShell.ActionDrawerOpenSurface {"
require_line "$status_panel" "virtualDesktopInfo: virtualDesktopInfo"
require_line "$action_open_surface" "property var virtualDesktopInfo: null"
require_line "$action_open_surface" "touchpadWorkspaceSwitchThreshold"
require_line "$action_open_surface" "function activateAdjacentWorkspace(direction)"
require_line "$action_open_surface" "virtualDesktopInfo.requestActivate(virtualDesktopInfo.desktopIds[targetIndex]);"
require_line "$action_open_surface" "absY >= touchpadDirectionLockThreshold && absY > absX * touchpadAxisDominance"
require_line "$action_open_surface" "workspaceScrollAvailable() && absX >= touchpadDirectionLockThreshold && absX > absY * touchpadAxisDominance"
require_line "$action_open_surface" "MouseArea {"
require_line "$action_open_surface" "acceptedButtons: Qt.NoButton"
require_line "$action_open_surface" "hoverEnabled: true"
require_line "$action_open_surface" "scrollGestureEnabled: false"
require_line "$action_open_surface" "cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor"
require_line "$swipearea_cpp" "const qreal WHEEL_STEP_PIXEL_DELTA = 48.0;"
require_line "$swipearea_cpp" "const int TOUCHPAD_SCROLL_END_TIMEOUT = 160;"
require_line "$swipearea_cpp" "const bool isTouchpad = event->deviceType() == QInputDevice::DeviceType::TouchPad;"
require_line "$swipearea_cpp" "startTouchpadScroll(event->points().first().position());"
require_line "$swipearea_cpp" "m_touchpadScrollEndTimer.start();"
require_line "$swipearea_cpp" "if (scrollDelta.isNull() && !event->angleDelta().isNull())"
require_line "$swipearea_cpp" "QWheelEvent::DefaultDeltasPerStep * WHEEL_STEP_PIXEL_DELTA"
require_line "$swipearea_header" "QTimer m_touchpadScrollEndTimer;"
require_line "$favourites_bar" "function handlePagerWheel(wheel)"
require_line "$favourites_bar" "onWheel: (wheel) => root.handlePagerWheel(wheel)"
require_line "$favourites_bar" "root.activateAdjacentDesktop(1)"
require_line "$favourites_bar" "readonly property int pagerLeftCount: showPager ? virtualDesktopInfo.numberOfDesktops : 0"
require_line "$favourites_bar" "readonly property int pagerRightCount: 0"
require_line "$favourites_bar" "readonly property real rightControlsWidth: convergenceMode ? navButtonWidth + searchButtonWidth + trashButtonWidth : 0"
require_line "$favourites_bar" "Virtual desktop pager: compact group next to the Desktop button"
if grep -Fq "Math.ceil(virtualDesktopInfo.numberOfDesktops / 2)" "$favourites_bar"; then
echo "Desktop pager must not split virtual desktops between dock edges" >&2
exit 1
fi
require_line "$folio_home" "onTouchpadScrollStarted: {"
require_line "$folio_home" "onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => {"
require_line "$folio_home" "if (!ShellSettings.Settings.convergenceModeEnabled) {"
require_line "$status_bar_template" "panel.location = \"top\";"
require_line "$taskpanel" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$taskpanel" "height: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness"
require_line "$taskpanel" "LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness"
require_line "$taskpanel" "visible: !root.fullscreen && !ShellSettings.Settings.convergenceModeEnabled"
require_line "$taskpanel" "LayerShell.Window.scope: \"workspace-frame-left\""
require_line "$taskpanel" "LayerShell.Window.scope: \"workspace-frame-right\""
require_line "$folio_main" "id: convergenceChrome"
require_line "$folio_main" "LayerShell.Window.scope: \"convergence-chrome\""
require_line "$folio_main" "height: Screen.height"
require_line "$folio_main" "MobileShell.StatusBar {"
require_line "$folio_main" "MobileShell.ShellUtil.setInputRegions(convergenceChrome, regions)"
require_line "$folio_main" "const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)"
require_line "$folio_main" "readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight"
require_line "$folio_main" "readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight"
require_line "$folio_main" "import QtQuick.Shapes 1.8"
require_line "$folio_main" "id: workspaceFrame"
require_line "$folio_main" "readonly property real frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real frameRadius:"
require_line "$folio_main" "readonly property real workAreaX: frameThickness"
require_line "$folio_main" "readonly property real topBarHitHeight: topBarHeight + frameThickness"
require_line "$folio_main" "readonly property real workAreaY: topBarHitHeight"
require_line "$folio_main" "readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)"
require_line "$folio_main" "readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2)"
require_line "$folio_main" "fillRule: ShapePath.OddEvenFill"
require_line "$folio_main" "id: leftEdgeHoverArea"
require_line "$folio_main" "id: rightEdgeHoverArea"
require_line "$folio_main" "cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor"
require_line "$folio_main" "convergenceChrome.leftEdgeHovered = containsMouse"
require_line "$folio_main" "convergenceChrome.rightEdgeHovered = containsMouse"
require_line "$folio_main" "convergenceChrome.leftLauncherHovered = hovered"
require_line "$folio_main" "convergenceChrome.layoutMenuHovered = hovered"
if grep -Fq "root.panel.visibilityMode = (!ShellSettings.Settings.convergenceModeEnabled && ShellSettings.Settings.autoHidePanelsEnabled) ? 3 : 0" "$panel"; then
echo "Convergence panel hit area must not reserve extra top space" >&2
exit 1
fi
if grep -Fq "id: leftLauncherPointerTracker" "$folio_main"; then
echo "Folio convergence chrome must not cover the dock with a fullscreen hover tracker" >&2
exit 1
fi
require_line "$folio_main" "PathLine { x: convergenceChrome.width; y: convergenceChrome.height - convergenceChrome.dockHeight }"
require_line "$folio_main" "PathLine { x: 0; y: convergenceChrome.height - convergenceChrome.dockHeight }"
require_line "$folio_main" "PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }"
require_line "$folio_main" "PathArc { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }"
require_line "$shellutil_header" "Q_INVOKABLE void setInputRegions(QWindow *window, const QVariantList &regions);"
require_line "$shellutil_cpp" "void ShellUtil::setInputRegions(QWindow *window, const QVariantList &regions)"
require_line "$shellutil_cpp" "for (const QVariant &value : regions)"
require_line "$folio_main" "readonly property real popupTopY: MobileShell.Constants.topPanelHeight"
require_line "$folio_main" "+ MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real popupBottomY: parent.height"
require_line "$folio_main" "- MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real popupHeight: Math.max(0, popupBottomY - popupTopY)"
require_line "$folio_main" "? popupTopY + animationY"
require_line "$folio_main" "id: drawerSurface"
require_line "$folio_main" "readonly property real bodyWidth: Math.max(0, powerPanel.x + powerPanel.width - overlayDrawer.x)"
require_line "$folio_main" "readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius"
require_line "$folio_main" "PathArc { x: drawerSurface.bodyWidth; y: drawerSurfacePath.cornerRadius; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }"
require_line "$folio_main" "PathArc { x: drawerSurface.bodyWidth + drawerSurfacePath.cornerRadius; y: drawerSurface.height; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }"
require_line "$folio_home" "height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6"
require_line "$folio_home" "&& !ShellSettings.Settings.convergenceModeEnabled"
require_line "$folio_backend" "Q_PROPERTY(bool overviewActive READ overviewActive NOTIFY overviewActiveChanged)"
overview_hide_guards="$(grep -F "&& !folio.overviewActive" "$folio_main" | wc -l)"
if [[ "$overview_hide_guards" -ne 1 ]]; then
echo "Expected Folio convergence chrome to hide during KWin Overview; found $overview_hide_guards overview guards" >&2
exit 1
fi
if ! awk '
/void HomeScreen::triggerOverview\(\)/ { in_trigger = 1; next }
in_trigger && /setOverviewActive\(true\);/ { saw_pre_hide = 1; next }
in_trigger && /createMethodCall/ {
if (saw_pre_hide) {
found = 1
exit 0
}
exit 1
}
in_trigger && /^}/ { exit 1 }
END { if (!found) exit 1 }
' "$folio_backend_cpp"; then
echo "Expected Folio to hide convergence chrome before invoking KWin Overview" >&2
exit 1
fi
require_line "$action_content" "readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight + convergenceFrameThickness"
require_line "$action_content" "readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness"
require_line "$action_content" "readonly property real convergenceSurfaceSideInset: 0"
require_line "$action_content" "readonly property real convergenceFloatingMargin: Kirigami.Units.gridUnit"
require_line "$action_content" "readonly property real convergenceClickAwayGutter: Kirigami.Units.largeSpacing"
require_line "$action_content" "readonly property real convergenceLeftSurfaceBottomInset: convergenceSurfaceBottomInset + convergenceBottomClickAwayHeight"
require_line "$action_content" "readonly property real convergenceQuickSettingsLeft: contentContainerLoader.item ? contentContainerLoader.item.quickSettingsPanelLeft"
require_line "$action_content" "readonly property real convergenceLeftSurfaceTopInset: convergenceSurfaceTopInset + convergenceFloatingMargin"
require_line "$action_content" "readonly property real convergenceLeftSurfaceLeftInset: convergenceSurfaceSideInset + convergenceFloatingMargin"
require_line "$action_content" "readonly property real convergenceLeftSurfaceRightInset: convergenceNotificationRightMargin + convergenceFloatingMargin"
require_line "$action_content" "readonly property real convergenceLeftSurfaceBottomMargin: convergenceLeftSurfaceBottomInset + convergenceFloatingMargin"
require_line "$action_content" "id: convergenceLeftSurface"
require_line "$action_content" "id: convergenceMediaPanel"
require_line "$action_content" "PlasmaCalendar.MonthView {"
require_line "$action_content" "visible: actionDrawer.intendedToBeVisible"
require_line "$action_content" "topMargin: isConvergence ? root.convergenceLeftSurfaceTopInset : 0"
require_line "$action_content" "leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceLeftSurfaceLeftInset"
require_line "$action_content" "maximumHeight: isConvergence ? root.convergenceSurfaceHeight * 0.32 : -1"
if grep -Fq "visible: !isConvergence" "$action_content"; then
echo "NotificationDrawer must remain visible in convergence so the action drawer shows notification history" >&2
exit 1
fi
if grep -Fq "visible: actionDrawer.intendedToBeVisible && !root.isConvergence" "$action_content"; then
echo "Clear-all notification toolbar must remain visible in convergence" >&2
exit 1
fi
require_line "$action_landscape" "readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight + convergenceFrameThickness"
require_line "$action_landscape" "readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness"
require_line "$action_landscape" "readonly property real convergenceSurfaceSideInset: 0"
require_line "$action_landscape" "readonly property real quickSettingsPanelLeft: quickSettingsPanel.x"
require_line "$action_landscape" "import QtQuick.Shapes 1.8"
require_line "$action_landscape" "id: actionDrawerSurface"
require_line "$action_landscape" "x: quickSettingsPanel.x - cornerRadius"
require_line "$action_landscape" "readonly property real bodyWidth: quickSettingsPanel.width"
require_line "$action_landscape" "PathArc { x: actionDrawerSurfacePath.cornerRadius; y: actionDrawerSurfacePath.cornerRadius; radiusX: actionDrawerSurfacePath.cornerRadius; radiusY: actionDrawerSurfacePath.cornerRadius; direction: PathArc.Clockwise }"
require_line "$action_landscape" "PathArc { x: 0; y: actionDrawerSurface.height; radiusX: actionDrawerSurfacePath.cornerRadius; radiusY: actionDrawerSurfacePath.cornerRadius; direction: PathArc.Clockwise }"
require_line "$action_landscape" "anchors.topMargin: isConvergence ? restingTopMargin"
require_line "$action_landscape" "anchors.rightMargin: isConvergence ? root.convergenceSurfaceSideInset : 0"
require_line "$action_landscape" "fullScreenHeight: quickSettingsPanel.availableHeight"
require_line "$quick_settings_panel" "readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled"
require_line "$quick_settings_panel" "readonly property real panelPadding: isConvergence ? Kirigami.Units.smallSpacing : Kirigami.Units.smallSpacing * 4"
require_line "$quick_settings_panel" "anchors.margins: root.isConvergence ? 0 : Kirigami.Units.largeSpacing"
require_line "$quick_settings_panel" "visible: !root.isConvergence"
require_line "$quick_settings_panel" "panelType: root.isConvergence ? MobileShell.PanelBackground.PanelType.Flat : MobileShell.PanelBackground.PanelType.Base"
require_line "$quick_settings_panel" "radius: root.isConvergence ? 0 : Kirigami.Units.cornerRadius"
require_line "$base_item" "implicitHeight: topPadding + bottomPadding + (contentItem ? contentItem.implicitHeight : 0)"
require_line "$base_item" "implicitWidth: leftPadding + rightPadding + (contentItem ? contentItem.implicitWidth : 0)"
require_line "$quick_settings" "id: convergenceFlow"
require_line "$quick_settings" "visible: root.isConvergence"
require_line "$quick_settings" "visible: !root.isConvergence"
require_line "$quick_settings" "active: !root.isConvergence && swipeView.count > 1 ? true: false"
if grep -Fq "id: convergenceNotificationHistory" "$quick_settings"; then
echo "Convergence notification history must not consume quick-settings stack height" >&2
exit 1
fi
if grep -Fq "NotificationHistoryPopup" "$quick_settings"; then
echo "Notification history must use the in-drawer NotificationDrawer, not a separate popup" >&2
exit 1
fi
require_line "$notification_popup_manager" "readonly property real convergencePopupMargin: MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing"
require_line "$notification_popup_manager" "readonly property real convergencePopupBottomInset: MobileShell.Constants.convergenceDockHeight + MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing"
require_line "$notification_popup_manager" "let regionY = (currentPopup ? currentPopup.effectiveOpenOffset : Screen.height - notificationPopupManager.convergencePopupBottomInset - popupHeight)"
require_line "$notification_popup_manager" "convergenceBottomInset: notificationPopupManager.convergencePopupBottomInset"
require_line "$notification_popup_manager" "? (notificationPopupManager.width - width - notificationPopupManager.convergencePopupMargin)"
require_line "$notification_popup" "property real convergenceBottomInset: openOffset"
require_line "$notification_popup" "? (Screen.height - convergenceBottomInset - popupHeight)"
require_line "$notification_popup_item" "icon.name: \"window-close\""
require_line "$notification_popup_item" "text: i18n(\"Dismiss\")"
require_line "$notification_card" "if (!contentItem) {"
require_line "$notification_drawer" "&& !ShellSettings.Settings.convergenceModeEnabled"
require_line "$notification_drawer" "emptyText: ShellSettings.Settings.convergenceModeEnabled ? i18n(\"No notifications\") : \"\""
require_line "$notifications_widget" "property string emptyText: \"\""
if grep -Fq "notificationItem.close();" "$notification_popup_item"; then
echo "Popup dismissals must expire the popup instead of closing notification history" >&2
exit 1
fi
if grep -Fq "contentParent.children.push(contentItem);" "$notification_card"; then
echo "NotificationCard content is already parented by assignment; do not push it into children again" >&2
exit 1
fi
frame_arc_count="$(grep -F "PathArc { x: convergenceChrome." "$folio_main" | wc -l || true)"
if [[ "$frame_arc_count" -ne 4 ]]; then
echo "Expected the workspace frame cutout to have four rounded inner corners; found $frame_arc_count arcs" >&2
exit 1
fi
if grep -Fq "convergenceWallpaperLayer" "$folio_main"; then
echo "Do not replace the real wallpaper with a custom convergence wallpaper layer" >&2
exit 1
fi
dock_offset_transforms="$(grep -F "transform: Translate { y: convergenceChrome.dockOffset }" "$folio_main" | wc -l || true)"
if [[ "$dock_offset_transforms" -ne 1 ]]; then
echo "Expected only dock contents to slide with convergenceChrome.dockOffset; found $dock_offset_transforms transforms" >&2
exit 1
fi