176 lines
3.8 KiB
QML
176 lines
3.8 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import qs.Modules.Settings
|
|
import qs.Components
|
|
import ZShell.Config
|
|
import qs.Services
|
|
|
|
ColumnLayout {
|
|
id: root
|
|
|
|
property bool animateScroll: false
|
|
readonly property int cappedWidth: Math.min(800, width)
|
|
default property Item contentChild
|
|
readonly property alias flickable: flickable
|
|
property bool isSubPage
|
|
required property SettingsState sState
|
|
required property string title
|
|
|
|
function applySearchAnchor(): void {
|
|
if (!sState.searchAnchor)
|
|
return;
|
|
scrollRetry.tries = 0;
|
|
scrollRetry.lastHeight = -1;
|
|
scrollRetry.stableFrames = 0;
|
|
scrollRetry.restart();
|
|
}
|
|
|
|
function findAnchor(item: Item, anchor: string): Item {
|
|
if (!item)
|
|
return null;
|
|
if (item.settingAnchor !== undefined && item.settingAnchor === anchor)
|
|
return item;
|
|
const kids = item.children;
|
|
for (let i = 0; i < kids.length; i++) {
|
|
const found = findAnchor(kids[i], anchor);
|
|
if (found)
|
|
return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function highlightAnchor(anchor: string): void {
|
|
const row = findAnchor(contentChild, anchor);
|
|
if (row && row.flashHighlight !== undefined)
|
|
row.flashHighlight();
|
|
}
|
|
|
|
function scrollToAnchor(anchor: string): bool {
|
|
if (!anchor || !contentChild)
|
|
return false;
|
|
const row = findAnchor(contentChild, anchor);
|
|
if (!row)
|
|
return false;
|
|
const pos = row.mapToItem(flickable.contentItem, 0, 0);
|
|
const inset = flickable.height * flickable.fadeAmount + Tokens.padding.large;
|
|
const minY = -flickable.topMargin;
|
|
const maxY = Math.max(minY, flickable.contentHeight + flickable.bottomMargin - flickable.height);
|
|
const target = Math.max(minY, Math.min(pos.y - inset, maxY));
|
|
root.animateScroll = true;
|
|
flickable.contentY = target;
|
|
Qt.callLater(() => root.animateScroll = false);
|
|
if (row.flashHighlight !== undefined)
|
|
row.flashHighlight();
|
|
return true;
|
|
}
|
|
|
|
spacing: Tokens.spacing.large
|
|
|
|
Component.onCompleted: applySearchAnchor()
|
|
|
|
Timer {
|
|
id: scrollRetry
|
|
|
|
property real lastHeight: -1
|
|
property int stableFrames: 0
|
|
property int tries: 0
|
|
|
|
interval: 16
|
|
repeat: true
|
|
|
|
onTriggered: {
|
|
const h = flickable.contentHeight;
|
|
if (h === lastHeight && h > flickable.height)
|
|
stableFrames++;
|
|
else
|
|
stableFrames = 0;
|
|
lastHeight = h;
|
|
|
|
const ready = stableFrames >= 3 || tries >= 30;
|
|
if (ready) {
|
|
if (root.scrollToAnchor(root.sState.searchAnchor))
|
|
root.sState.searchAnchor = "";
|
|
stop();
|
|
}
|
|
tries++;
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
function onHighlightSetting(anchor: string): void {
|
|
root.highlightAnchor(anchor);
|
|
}
|
|
|
|
function onSearchAnchorChanged(): void {
|
|
root.applySearchAnchor();
|
|
}
|
|
|
|
target: root.sState
|
|
}
|
|
|
|
MouseArea {
|
|
Layout.bottomMargin: -flickable.topMargin
|
|
implicitHeight: header.implicitHeight - Layout.bottomMargin
|
|
implicitWidth: header.implicitWidth
|
|
z: 1
|
|
|
|
onClicked: focus = true
|
|
|
|
RowLayout {
|
|
id: header
|
|
|
|
spacing: Tokens.spacing.large
|
|
|
|
Loader {
|
|
active: root.isSubPage
|
|
asynchronous: true
|
|
visible: active
|
|
|
|
sourceComponent: IconButton {
|
|
icon: "arrow_back"
|
|
inactiveColor: Colors.tPalette.m3surfaceContainerHigh
|
|
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
|
isRound: true
|
|
type: IconButton.Tonal
|
|
|
|
onClicked: root.sState.closeSubPage()
|
|
}
|
|
}
|
|
|
|
CustomText {
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
font.pointSize: Tokens.font.size.large
|
|
text: root.title
|
|
}
|
|
}
|
|
}
|
|
|
|
VerticalFadeFlickable {
|
|
id: flickable
|
|
|
|
Layout.fillHeight: true
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: -topMargin
|
|
bottomMargin: Tokens.padding.extraLarge
|
|
contentHeight: root.contentChild?.implicitHeight ?? 0
|
|
contentItem.children: [root.contentChild]
|
|
fadeAmount: 0.1
|
|
topMargin: Tokens.padding.large
|
|
|
|
Behavior on contentY {
|
|
enabled: root.animateScroll
|
|
|
|
Anim {
|
|
type: Anim.DefaultSpatial
|
|
}
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: flickable.focus = true
|
|
}
|
|
}
|
|
}
|