Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shell/services/PluginAppLibraryApi.qml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ QtObject {
required property string ownerPluginId

signal appsChanged()
signal iconIndexChanged()

property var _entryName: null
property var _entrySubtext: null
Expand Down
8 changes: 7 additions & 1 deletion shell/shell.qml
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,10 @@ ShellRoot {

Connections {
target: shell.appLibrary
function onIconIndexChanged() {
for (var id in shell._pluginAppLibraryApis)
shell._pluginAppLibraryApis[id].iconIndexChanged()
}
function onAppsChanged() {
for (var id in shell._pluginAppLibraryApis)
shell._pluginAppLibraryApis[id].appsChanged()
Expand Down Expand Up @@ -1316,7 +1320,9 @@ ShellRoot {
id: panelEntry
required property var modelData
readonly property string pluginId: modelData.id
readonly property var manifest: modelData.manifest
// Qt model data turns nested arrays into sequences. Use the registry
// object so capability checks see the validated manifest's array types.
readonly property var manifest: shell.pluginRegistry.installedPlugins[pluginId] || null
readonly property string entryKind: modelData.kind
readonly property bool keepLoaded: modelData.keepLoaded === true
readonly property string sourceUrl: shell.pluginRegistry.entryPointUrl(manifest, entryKind)
Expand Down
87 changes: 87 additions & 0 deletions test/shell.d/fixtures/panel-menu-capability/shell.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import QtQuick
import QtQml.Models
import Quickshell
import "services"

ShellRoot {
id: shell

property var pluginRegistry: ({ installedPlugins: {
"example.menu": { id: "example.menu", kinds: ["menu", "bar-widget"] },
"example.panel": { id: "example.panel", kinds: ["panel"] }
} })
property var panelEntries: Object.keys(pluginRegistry.installedPlugins).map(function(id) {
return { id: id, manifest: shell.pluginRegistry.installedPlugins[id] }
})
property var failures: []
property int checked: 0
property int iconCompletions: 0
property int appChanges: 0
property QtObject appLibrary: QtObject {
property var iconIndex: ({})
signal appsChanged()
}
property var _pluginAppLibraryApis: ({ "example.menu": menuLibrary })

__APP_LIBRARY_SIGNALS__

// Filled from shell.qml by the test runner, not a copy of the implementation.
__MANIFEST_HAS_KIND__

PluginAppLibraryApi {
id: menuLibrary
ownerPluginId: "example.menu"
_sortedEntries: function(query) { return [{ entry: { id: "example-app", name: "Example App" } }] }
}

function pluginAppLibraryFor(cacheKey, key) { return menuLibrary }

Connections {
target: menuLibrary
function onIconIndexChanged() { shell.iconCompletions += 1 }
function onAppsChanged() { shell.appChanges += 1 }
}

Component { id: apiComponent; PluginShellApi {} }

function check(manifest, pluginId) {
var key = pluginId
var cacheKey = key
var api = apiComponent.createObject(null, {
pluginId: key,
appLibrary: __APP_LIBRARY_GRANT__
})
var isMenu = pluginId === "example.menu"
var granted = !!api && !!api.appLibrary
if (granted !== isMenu) failures.push(pluginId + ": wrong app-library grant")
if (granted && api.appLibrary.sortedEntries("")[0].entry.id !== "example-app")
failures.push(pluginId + ": app entries unavailable")
if (api) api.destroy()
checked += 1
}

Instantiator {
model: shell.panelEntries
delegate: QtObject {
required property var modelData
readonly property string pluginId: modelData.id
__PANEL_MANIFEST_BINDING__
Component.onCompleted: shell.check(manifest, pluginId)
}
}

Timer {
interval: 50
running: true
onTriggered: {
shell.appLibrary.iconIndex = ({ updated: true })
shell.appLibrary.appsChanged()
if (shell.iconCompletions !== 1) shell.failures.push("icon refresh completion was not forwarded")
if (shell.appChanges !== 1) shell.failures.push("app changes were not forwarded")
if (shell.checked !== 2) shell.failures.push("both plugin kinds must be checked")
if (shell.failures.length) console.error("PANEL_MENU_FAIL", JSON.stringify(shell.failures))
else console.log("PANEL_MENU_OK menu receives apps; ordinary panel stays restricted")
Qt.quit()
}
}
}
51 changes: 51 additions & 0 deletions test/shell.d/panel-menu-capability-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/bash

set -euo pipefail

source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"

if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping panel menu capability runtime test"
exit 0
fi
require_command python3
require_command timeout

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
mkdir "$tmpdir/services"
cp "$ROOT/shell/services/PluginShellApi.qml" "$ROOT/shell/services/PluginAppLibraryApi.qml" "$tmpdir/services/"

# Exercise the production binding and grant across a real Qt model boundary.
# Node alone cannot reproduce Qt's conversion of nested arrays to sequences.
python3 - "$ROOT" "$tmpdir/shell.qml" <<'PY'
from pathlib import Path
import re
import sys

root = Path(sys.argv[1])
source = (root / "shell/shell.qml").read_text()
fixture = (root / "test/shell.d/fixtures/panel-menu-capability/shell.qml").read_text()
panel = source[source.index("id: panelEntry"):]
replacements = {
"__PANEL_MANIFEST_BINDING__": re.search(r"readonly property var manifest: [^\n]+", panel).group(),
"__MANIFEST_HAS_KIND__": re.search(r"function manifestHasKind\([^)]*\) \{.*?\n \}", source, re.S).group(),
"__APP_LIBRARY_GRANT__": re.search(r"appLibrary: (shell\.manifestHasKind\(manifest, \"menu\"\).*?),\n", source, re.S).group(1),
"__APP_LIBRARY_SIGNALS__": re.search(r"Connections \{\s+target: shell\.appLibrary\n.*?\n \}", source, re.S).group(),
}
for marker, code in replacements.items():
fixture = fixture.replace(marker, code)
Path(sys.argv[2]).write_text(fixture)
PY

# No windows or Wayland services: this test also runs with Qt's offscreen backend.
if ! env -u WAYLAND_DISPLAY QT_QPA_PLATFORM=offscreen \
timeout 8 quickshell --no-color -p "$tmpdir" >"$tmpdir/log" 2>&1; then
fail "panel menu capability fixture runs" "$(<"$tmpdir/log")"
fi
if ! grep -q 'PANEL_MENU_OK' "$tmpdir/log"; then
fail "menu plugins retain their app library across the Qt model boundary" "$(<"$tmpdir/log")"
fi
pass "menu plugins retain their app library across the Qt model boundary"
pass "ordinary panels cannot access the app library"
pass "app and icon refresh signals reach menu plugins"