Make Settings sidebar content scrollable
This commit is contained in:
@@ -11,6 +11,11 @@ Rectangle {
|
|||||||
|
|
||||||
readonly property var results: SettingsSearch.search(root.query)
|
readonly property var results: SettingsSearch.search(root.query)
|
||||||
|
|
||||||
|
onQueryChanged: {
|
||||||
|
if (sidebarScroll)
|
||||||
|
sidebarScroll.contentY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
function pageLabel(page: string): string {
|
function pageLabel(page: string): string {
|
||||||
const found = root.destinations.find(item => item.page === page);
|
const found = root.destinations.find(item => item.page === page);
|
||||||
return found ? found.label : "Settings";
|
return found ? found.label : "Settings";
|
||||||
@@ -40,8 +45,15 @@ Rectangle {
|
|||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
id: sidebarHeader
|
||||||
anchors.margins: 18
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: 18
|
||||||
|
anchors.rightMargin: 18
|
||||||
|
anchors.topMargin: 18
|
||||||
|
height: implicitHeight
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
@@ -103,149 +115,175 @@ Rectangle {
|
|||||||
onClicked: searchInput.forceActiveFocus()
|
onClicked: searchInput.forceActiveFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: sidebarScroll
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: sidebarHeader.bottom
|
||||||
|
anchors.bottom: healthFooter.top
|
||||||
|
anchors.leftMargin: 18
|
||||||
|
anchors.rightMargin: 18
|
||||||
|
anchors.topMargin: 12
|
||||||
|
anchors.bottomMargin: 12
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: scrollContent.implicitHeight
|
||||||
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
clip: true
|
||||||
|
|
||||||
// ── Search results ──────────────────────────────────────────────────
|
|
||||||
// Typing searches the settings themselves, not the twelve page names.
|
|
||||||
// "gaps", "wallpaper", and "screenshot" all used to find nothing, which
|
|
||||||
// made the app feel far smaller than it is.
|
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
id: scrollContent
|
||||||
spacing: 3
|
|
||||||
visible: root.query !== ""
|
width: sidebarScroll.width
|
||||||
|
|
||||||
|
// ── Search results ──────────────────────────────────────────────
|
||||||
|
// Typing searches the settings themselves, not page names.
|
||||||
|
Column {
|
||||||
|
id: searchResults
|
||||||
|
|
||||||
Text {
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
leftPadding: 4
|
spacing: 3
|
||||||
bottomPadding: 4
|
visible: root.query !== ""
|
||||||
text: root.results.length === 0
|
|
||||||
? "Nothing matches"
|
|
||||||
: root.results.length + (root.results.length === 1 ? " result" : " results")
|
|
||||||
color: Theme.fgMuted
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.results
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: hit
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
|
Text {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 44
|
leftPadding: 4
|
||||||
radius: 10
|
bottomPadding: 4
|
||||||
color: hitMouse.containsMouse ? Theme.alpha(Theme.fg, 0.08) : "transparent"
|
text: root.results.length === 0
|
||||||
border.width: 0
|
? "Nothing matches"
|
||||||
|
: root.results.length + (root.results.length === 1 ? " result" : " results")
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Repeater {
|
||||||
anchors.left: parent.left
|
model: root.results
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.leftMargin: 12
|
|
||||||
anchors.rightMargin: 10
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Text {
|
Rectangle {
|
||||||
width: parent.width
|
id: hit
|
||||||
text: hit.modelData.label
|
|
||||||
color: Theme.fg
|
required property var modelData
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSize
|
width: parent.width
|
||||||
elide: Text.ElideRight
|
height: 44
|
||||||
|
radius: 10
|
||||||
|
color: hitMouse.containsMouse ? Theme.alpha(Theme.fg, 0.08) : "transparent"
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: hit.modelData.label
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: hit.modelData.kind === "shortcut"
|
||||||
|
? hit.modelData.detail
|
||||||
|
: root.pageLabel(hit.modelData.page)
|
||||||
|
color: hit.modelData.kind === "shortcut" ? Theme.accent : Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
MouseArea {
|
||||||
width: parent.width
|
id: hitMouse
|
||||||
text: hit.modelData.kind === "shortcut"
|
anchors.fill: parent
|
||||||
? hit.modelData.detail
|
hoverEnabled: true
|
||||||
: root.pageLabel(hit.modelData.page)
|
cursorShape: Qt.PointingHandCursor
|
||||||
color: hit.modelData.kind === "shortcut" ? Theme.accent : Theme.fgMuted
|
onClicked: {
|
||||||
font.family: Theme.fontFamily
|
root.pageRequested(hit.modelData.page);
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
searchInput.text = "";
|
||||||
elide: Text.ElideRight
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: hitMouse
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.pageRequested(hit.modelData.page);
|
|
||||||
searchInput.text = "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
id: navigationList
|
||||||
spacing: 4
|
|
||||||
visible: root.query === ""
|
|
||||||
|
|
||||||
Repeater {
|
width: parent.width
|
||||||
model: root.destinations
|
spacing: 4
|
||||||
|
visible: root.query === ""
|
||||||
|
|
||||||
Rectangle {
|
Repeater {
|
||||||
id: navItem
|
model: root.destinations
|
||||||
required property var modelData
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
radius: 10
|
|
||||||
color: modelData.page === root.selectedPage
|
|
||||||
? Theme.alpha(Theme.accent, 0.17)
|
|
||||||
: (navMouse.containsMouse ? Theme.alpha(Theme.fg, Theme.hoverAlpha * 0.55) : Theme.alpha(Theme.fg, 0))
|
|
||||||
border.width: modelData.page === root.selectedPage ? 1 : 0
|
|
||||||
border.color: Theme.alpha(Theme.accent, 0.26)
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 2
|
id: navItem
|
||||||
height: 18
|
required property var modelData
|
||||||
radius: 1
|
width: parent.width
|
||||||
anchors.left: parent.left
|
height: 40
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
radius: 10
|
||||||
visible: navItem.modelData.page === root.selectedPage
|
color: modelData.page === root.selectedPage
|
||||||
gradient: Gradient {
|
? Theme.alpha(Theme.accent, 0.17)
|
||||||
GradientStop { position: 0; color: Theme.accent }
|
: (navMouse.containsMouse ? Theme.alpha(Theme.fg, Theme.hoverAlpha * 0.55) : Theme.alpha(Theme.fg, 0))
|
||||||
GradientStop { position: 1; color: Theme.accentSecondary }
|
border.width: modelData.page === root.selectedPage ? 1 : 0
|
||||||
|
border.color: Theme.alpha(Theme.accent, 0.26)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 2
|
||||||
|
height: 18
|
||||||
|
radius: 1
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: navItem.modelData.page === root.selectedPage
|
||||||
|
gradient: Gradient {
|
||||||
|
GradientStop { position: 0; color: Theme.accent }
|
||||||
|
GradientStop { position: 1; color: Theme.accentSecondary }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 13
|
anchors.leftMargin: 13
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: 25
|
width: 25
|
||||||
text: navItem.modelData.icon
|
text: navItem.modelData.icon
|
||||||
color: navItem.modelData.page === root.selectedPage ? Theme.accent : Theme.fgDim
|
color: navItem.modelData.page === root.selectedPage ? Theme.accent : Theme.fgDim
|
||||||
font.family: Theme.fontMono
|
font.family: Theme.fontMono
|
||||||
font.pixelSize: 15
|
font.pixelSize: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 47
|
anchors.leftMargin: 47
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: 9
|
anchors.rightMargin: 9
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: navItem.modelData.label
|
text: navItem.modelData.label
|
||||||
color: navItem.modelData.page === root.selectedPage ? Theme.fg : Theme.fgDim
|
color: navItem.modelData.page === root.selectedPage ? Theme.fg : Theme.fgDim
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSize
|
font.pixelSize: Theme.fontSize
|
||||||
font.weight: navItem.modelData.page === root.selectedPage ? Font.Medium : Font.Normal
|
font.weight: navItem.modelData.page === root.selectedPage ? Font.Medium : Font.Normal
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: navMouse
|
id: navMouse
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: root.pageRequested(navItem.modelData.page)
|
onClicked: root.pageRequested(navItem.modelData.page)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,6 +291,8 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: healthFooter
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
|
|||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
sidebar="$repo_dir/config/dot/quickshell/modules/settings/SettingsSidebar.qml"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'settings sidebar layout contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
python3 - "$sidebar" <<'PY' || fail 'sidebar does not keep its header and footer pinned around one vertical scroll surface'
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
text = open(sys.argv[1], encoding="utf-8").read()
|
||||||
|
|
||||||
|
|
||||||
|
def object_block(type_name: str, object_id: str) -> tuple[int, int, str]:
|
||||||
|
pattern = re.compile(
|
||||||
|
rf"\b{re.escape(type_name)}\s*\{{(?:(?!\n\s*[A-Za-z][A-Za-z0-9.]*\s*\{{).)*?"
|
||||||
|
rf"\bid\s*:\s*{re.escape(object_id)}\b",
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
match = pattern.search(text)
|
||||||
|
if not match:
|
||||||
|
raise AssertionError(f"missing {type_name} id {object_id}")
|
||||||
|
|
||||||
|
start = match.start()
|
||||||
|
opening = text.index("{", start)
|
||||||
|
depth = 0
|
||||||
|
in_string = False
|
||||||
|
escaped = False
|
||||||
|
index = opening
|
||||||
|
while index < len(text):
|
||||||
|
character = text[index]
|
||||||
|
if in_string:
|
||||||
|
if escaped:
|
||||||
|
escaped = False
|
||||||
|
elif character == "\\":
|
||||||
|
escaped = True
|
||||||
|
elif character == '"':
|
||||||
|
in_string = False
|
||||||
|
elif character == '"':
|
||||||
|
in_string = True
|
||||||
|
elif character == "{":
|
||||||
|
depth += 1
|
||||||
|
elif character == "}":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
return start, index + 1, text[start:index + 1]
|
||||||
|
index += 1
|
||||||
|
raise AssertionError(f"unterminated {type_name} id {object_id}")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
header_start, header_end, header = object_block("Column", "sidebarHeader")
|
||||||
|
scroll_start, scroll_end, scroll = object_block("Flickable", "sidebarScroll")
|
||||||
|
footer_start, _, footer = object_block("Rectangle", "healthFooter")
|
||||||
|
|
||||||
|
assert header_start < scroll_start < scroll_end < footer_start
|
||||||
|
assert re.search(r"anchors\.top\s*:\s*parent\.top", header)
|
||||||
|
assert re.search(r"\bid\s*:\s*searchInput\b", header)
|
||||||
|
|
||||||
|
assert re.search(r"anchors\.top\s*:\s*sidebarHeader\.bottom", scroll)
|
||||||
|
assert re.search(r"anchors\.bottom\s*:\s*healthFooter\.top", scroll)
|
||||||
|
assert re.search(r"contentWidth\s*:\s*width", scroll)
|
||||||
|
assert re.search(r"contentHeight\s*:\s*scrollContent\.implicitHeight", scroll)
|
||||||
|
assert re.search(r"flickableDirection\s*:\s*Flickable\.VerticalFlick", scroll)
|
||||||
|
assert re.search(r"boundsBehavior\s*:\s*Flickable\.StopAtBounds", scroll)
|
||||||
|
assert re.search(r"clip\s*:\s*true", scroll)
|
||||||
|
assert scroll.count("Flickable {") == 1
|
||||||
|
|
||||||
|
assert re.search(r"\bid\s*:\s*scrollContent\b", scroll)
|
||||||
|
assert re.search(r"\bid\s*:\s*searchResults\b", scroll)
|
||||||
|
assert re.search(r"\bid\s*:\s*navigationList\b", scroll)
|
||||||
|
assert re.search(r"visible\s*:\s*root\.query\s*!==\s*\"\"", scroll)
|
||||||
|
assert re.search(r"visible\s*:\s*root\.query\s*===\s*\"\"", scroll)
|
||||||
|
assert re.search(r"anchors\.bottom\s*:\s*parent\.bottom", footer)
|
||||||
|
except AssertionError as error:
|
||||||
|
print(error, file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
|
PY
|
||||||
|
|
||||||
|
printf 'settings sidebar layout contract: PASS\n'
|
||||||
Reference in New Issue
Block a user