NGridView + NScrollView + NListView: everywhere, with auto top/bottom gradients.

This commit is contained in:
Lemmy
2026-01-24 22:08:34 -05:00
parent 4e3450c22c
commit 8e6a88b559
13 changed files with 301 additions and 404 deletions
+2 -4
View File
@@ -1300,14 +1300,12 @@ SmartPanel {
}
}
ScrollView {
NScrollView {
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.topMargin: Style.fontSizeL + Style.marginXL
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentWidth: availableWidth
horizontalPolicy: ScrollBar.AlwaysOff
NText {
width: parent.width
@@ -541,39 +541,6 @@ SmartPanel {
}
}
}
// Overlay gradient to smooth the hard cut due to scrolling at the bottom (only visible when scrollable)
Rectangle {
anchors.fill: parent
color: "transparent"
visible: scrollView.ScrollBar.vertical && scrollView.ScrollBar.vertical.size < 1.0
opacity: {
const scrollBar = scrollView.ScrollBar.vertical;
return (scrollBar.position + scrollBar.size >= 0.99) ? 0 : 1;
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 0.85
color: "transparent"
}
GradientStop {
position: 1.0
color: Qt.alpha(Color.mSurface, 0.95)
}
}
}
}
}
}
@@ -96,11 +96,12 @@ ColumnLayout {
}
// List of current blacklist items
ListView {
NListView {
Layout.fillWidth: true
Layout.preferredHeight: 150
Layout.topMargin: Style.marginL // Increased top margin
clip: true
gradientColor: Color.mSurface
model: blacklistModel
delegate: Item {
width: ListView.width
+32 -96
View File
@@ -803,6 +803,8 @@ Item {
spacing: Style.marginXS
visible: root.searchText.trim() !== ""
verticalPolicy: ScrollBar.AsNeeded
gradientColor: Color.mSurface
reserveScrollbarSpace: false
HoverHandler {
onPointChanged: {
@@ -901,6 +903,8 @@ Item {
spacing: Style.marginXS
currentIndex: root.currentTabIndex
verticalPolicy: ScrollBar.AsNeeded
gradientColor: Color.mSurface
reserveScrollbarSpace: false
delegate: Rectangle {
id: tabItem
@@ -1014,38 +1018,6 @@ Item {
}
}
}
// Overlay gradient for sidebar scrolling
Rectangle {
anchors.fill: parent
anchors.margins: Style.borderS
radius: Style.radiusM
color: "transparent"
visible: sidebarList.verticalScrollBarActive
opacity: (sidebarList.contentY + sidebarList.height >= sidebarList.contentHeight - 10) ? 0 : 1
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 0.95
color: "transparent"
}
GradientStop {
position: 1.0
color: Color.mSurfaceVariant
}
}
}
}
// Content pane
@@ -1128,39 +1100,38 @@ Item {
}
}
sourceComponent: Flickable {
id: flickable
sourceComponent: NScrollView {
id: scrollView
anchors.fill: parent
pressDelay: 200
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
leftPadding: Style.marginL
topPadding: Style.marginL
bottomPadding: Style.marginL
userRightPadding: Style.marginL
reserveScrollbarSpace: false
NScrollView {
id: scrollView
anchors.fill: parent
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
padding: Style.marginL
Component.onCompleted: {
root.activeScrollView = scrollView;
}
Component.onCompleted: {
root.activeScrollView = scrollView;
}
Loader {
active: true
sourceComponent: root.tabsModel[index]?.source
width: scrollView.availableWidth
onLoaded: {
if (item && item.hasOwnProperty("screen")) {
item.screen = root.screen;
}
root.activeTabContent = item;
// Handle pending subtab + highlight from search navigation
if (root.highlightLabelKey) {
if (root._pendingSubTab >= 0) {
root.setSubTabIndex(root._pendingSubTab);
root._pendingSubTab = -1;
}
highlightScrollTimer.targetKey = root.highlightLabelKey;
highlightScrollTimer.restart();
Loader {
active: true
sourceComponent: root.tabsModel[index]?.source
width: scrollView.availableWidth
onLoaded: {
if (item && item.hasOwnProperty("screen")) {
item.screen = root.screen;
}
root.activeTabContent = item;
// Handle pending subtab + highlight from search navigation
if (root.highlightLabelKey) {
if (root._pendingSubTab >= 0) {
root.setSubTabIndex(root._pendingSubTab);
root._pendingSubTab = -1;
}
highlightScrollTimer.targetKey = root.highlightLabelKey;
highlightScrollTimer.restart();
}
}
}
@@ -1168,41 +1139,6 @@ Item {
}
}
// Overlay gradient for content scrolling
Rectangle {
anchors.fill: parent
color: "transparent"
visible: root.activeScrollView && root.activeScrollView.ScrollBar.vertical && root.activeScrollView.ScrollBar.vertical.size < 1.0
opacity: {
if (!root.activeScrollView)
return 1;
const scrollBar = root.activeScrollView.ScrollBar.vertical;
return (scrollBar.position + scrollBar.size >= 0.99) ? 0 : 1;
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 0.95
color: "transparent"
}
GradientStop {
position: 1.0
color: Qt.alpha(Color.mSurfaceVariant, 0.95)
}
}
}
// Highlight overlay for search results
Rectangle {
id: highlightOverlay
@@ -19,12 +19,11 @@ ColumnLayout {
Layout.fillWidth: true
implicitHeight: listView.contentHeight
ListView {
NListView {
id: listView
anchors.fill: parent
spacing: Style.marginS
interactive: false
clip: true
model: root.entriesModel
delegate: Item {
@@ -87,13 +87,11 @@ ColumnLayout {
}
}
ScrollView {
NScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
contentWidth: availableWidth
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
ColumnLayout {
width: parent.width
@@ -56,13 +56,11 @@ ColumnLayout {
}
}
ScrollView {
NScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
contentWidth: availableWidth
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
ColumnLayout {
width: parent.width
@@ -135,15 +135,16 @@ ColumnLayout {
// Wallpaper gallery strip
Item {
Layout.fillWidth: true
Layout.preferredHeight: 88
Layout.preferredHeight: 92
visible: filteredWallpapers.length > 0
ScrollView {
NScrollView {
id: galleryScroll
anchors.fill: parent
clip: true
ScrollBar.horizontal.policy: ScrollBar.AsNeeded
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
horizontalPolicy: ScrollBar.AsNeeded
verticalPolicy: ScrollBar.AlwaysOff
showGradientMasks: false
reserveScrollbarSpace: false
// Enable vertical mouse wheel to scroll the horizontal strip by moving contentX
WheelHandler {
+24 -176
View File
@@ -46,7 +46,7 @@ SmartPanel {
return;
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
if (view?.gridView) {
if (!view.gridView.activeFocus) {
if (!view.gridView.hasActiveFocus) {
view.gridView.forceActiveFocus();
if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) {
view.gridView.currentIndex = 0;
@@ -65,7 +65,7 @@ SmartPanel {
if (!contentItem)
return;
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
if (view?.gridView?.activeFocus) {
if (view?.gridView?.hasActiveFocus) {
if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) {
view.gridView.currentIndex = 0;
} else {
@@ -78,7 +78,7 @@ SmartPanel {
if (!contentItem)
return;
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
if (view?.gridView?.activeFocus) {
if (view?.gridView?.hasActiveFocus) {
if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) {
view.gridView.currentIndex = 0;
} else {
@@ -91,7 +91,7 @@ SmartPanel {
if (!contentItem)
return;
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
if (view?.gridView?.activeFocus) {
if (view?.gridView?.hasActiveFocus) {
if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) {
view.gridView.currentIndex = 0;
} else {
@@ -104,7 +104,7 @@ SmartPanel {
if (!contentItem)
return;
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex);
if (view?.gridView?.activeFocus) {
if (view?.gridView?.hasActiveFocus) {
let gridView = view.gridView;
if (gridView.currentIndex >= 0 && gridView.currentIndex < gridView.model.length) {
view.selectItem(gridView.model[gridView.currentIndex]);
@@ -534,51 +534,6 @@ SmartPanel {
id: wallhavenView
}
}
// Overlay gradient to smooth the hard cut due to scrolling
Rectangle {
anchors.fill: parent
anchors.margins: Style.borderS
radius: Style.radiusM
// Get active grid view for scroll position
readonly property var activeGridView: {
if (Settings.data.wallpaper.useWallhaven) {
return wallhavenView.gridView;
} else {
const view = screenRepeater.itemAt(currentScreenIndex);
return view?.gridView ?? null;
}
}
opacity: {
if (!activeGridView)
return 1;
return (activeGridView.contentY + activeGridView.height >= activeGridView.contentHeight - 10) ? 0 : 1;
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 0.9
color: "transparent"
}
GradientStop {
position: 1.0
color: Color.mSurfaceVariant
}
}
}
}
}
}
@@ -832,7 +787,7 @@ SmartPanel {
}
}
GridView {
NGridView {
id: wallpaperGridView
Layout.fillWidth: true
@@ -840,10 +795,9 @@ SmartPanel {
visible: !WallpaperService.scanning
interactive: true
clip: true
focus: true
keyNavigationEnabled: true
keyNavigationWraps: false
highlightFollowsCurrentItem: false
currentIndex: -1
model: filteredItems
@@ -858,19 +812,10 @@ SmartPanel {
positionViewAtBeginning();
}
// Capture clicks on empty areas to give focus to GridView
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
wallpaperGridView.forceActiveFocus();
}
}
property int columns: (screen.width > 1920) ? 5 : 4
property int itemSize: cellWidth
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellWidth: Math.floor((availableWidth - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.7) + Style.marginXS + Style.fontSizeXS + Style.marginM
leftMargin: Style.marginS
@@ -895,62 +840,14 @@ SmartPanel {
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Space) {
if (currentIndex >= 0 && currentIndex < filteredItems.length) {
selectItem(filteredItems[currentIndex]);
}
event.accepted = true;
onKeyPressed: event => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Space) {
if (currentIndex >= 0 && currentIndex < filteredItems.length) {
selectItem(filteredItems[currentIndex]);
}
event.accepted = true;
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
parent: wallpaperGridView
x: wallpaperGridView.mirrored ? 0 : wallpaperGridView.width - width
y: 0
height: wallpaperGridView.height
property color handleColor: Qt.alpha(Color.mHover, 0.8)
property color handleHoverColor: handleColor
property color handlePressedColor: handleColor
property real handleWidth: 6
property real handleRadius: Style.radiusM
contentItem: Rectangle {
implicitWidth: parent.handleWidth
implicitHeight: 100
radius: parent.handleRadius
color: parent.pressed ? parent.handlePressedColor : parent.hovered ? parent.handleHoverColor : parent.handleColor
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: parent.handleWidth
implicitHeight: 100
color: "transparent"
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
radius: parent.handleRadius / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}
delegate: Item {
id: wallpaperItemWrapper
@@ -1253,17 +1150,16 @@ SmartPanel {
Layout.fillWidth: true
Layout.fillHeight: true
GridView {
NGridView {
id: wallhavenGridView
anchors.fill: parent
visible: !loading && errorMessage === "" && (wallpapers && wallpapers.length > 0)
interactive: true
clip: true
focus: true
keyNavigationEnabled: true
keyNavigationWraps: false
highlightFollowsCurrentItem: false
currentIndex: -1
model: wallpapers || []
@@ -1281,7 +1177,7 @@ SmartPanel {
property int columns: (screen.width > 1920) ? 5 : 4
property int itemSize: cellWidth
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellWidth: Math.floor((availableWidth - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.7) + Style.marginXS + Style.fontSizeXS + Style.marginM
leftMargin: Style.marginS
@@ -1304,63 +1200,15 @@ SmartPanel {
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Space) {
if (currentIndex >= 0 && currentIndex < wallpapers.length) {
let wallpaper = wallpapers[currentIndex];
wallhavenDownloadAndApply(wallpaper);
}
event.accepted = true;
onKeyPressed: event => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Space) {
if (currentIndex >= 0 && currentIndex < wallpapers.length) {
let wallpaper = wallpapers[currentIndex];
wallhavenDownloadAndApply(wallpaper);
}
event.accepted = true;
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
parent: wallhavenGridView
x: wallhavenGridView.mirrored ? 0 : wallhavenGridView.width - width
y: 0
height: wallhavenGridView.height
property color handleColor: Qt.alpha(Color.mHover, 0.8)
property color handleHoverColor: handleColor
property color handlePressedColor: handleColor
property real handleWidth: 6
property real handleRadius: Style.radiusM
contentItem: Rectangle {
implicitWidth: parent.handleWidth
implicitHeight: 100
radius: parent.handleRadius
color: parent.pressed ? parent.handlePressedColor : parent.hovered ? parent.handleHoverColor : parent.handleColor
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: parent.handleWidth
implicitHeight: 100
color: "transparent"
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
radius: parent.handleRadius / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}
delegate: Item {
id: wallhavenItemWrapper
+6 -49
View File
@@ -402,57 +402,20 @@ Popup {
id: filteredModel
}
// Common scroll bar component
Component {
id: scrollBarComponent
ScrollBar {
policy: ScrollBar.AsNeeded
contentItem: Rectangle {
implicitWidth: 6
implicitHeight: 100
radius: Style.iRadiusM
color: Qt.alpha(Color.mHover, 0.8)
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: 6
implicitHeight: 100
color: "transparent"
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
radius: (Style.iRadiusM) / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}
// Grid view
GridView {
NGridView {
id: gridView
anchors.fill: parent
anchors.margins: Style.marginM
model: filteredModel
visible: filePickerPanel.viewMode
clip: true
reuseItems: true
gradientColor: Color.mSurface
property int columns: Math.max(1, Math.floor(width / (120)))
property int itemSize: Math.floor((width - leftMargin - rightMargin - (columns * Style.marginS)) / columns)
property int columns: Math.max(1, Math.floor(availableWidth / 120))
property int itemSize: Math.floor((availableWidth - leftMargin - rightMargin - (columns * Style.marginS)) / columns)
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellWidth: Math.floor((availableWidth - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.8) + Style.marginXS + Style.fontSizeS + Style.marginM
leftMargin: Style.marginS
@@ -460,13 +423,6 @@ Popup {
topMargin: Style.marginS
bottomMargin: Style.marginS
ScrollBar.vertical: scrollBarComponent.createObject(gridView, {
"parent": gridView,
"x": gridView.mirrored ? 0 : gridView.width - width,
"y": 0,
"height": gridView.height
})
delegate: Rectangle {
id: gridItem
width: gridView.itemSize
@@ -660,6 +616,7 @@ Popup {
anchors.margins: Style.marginS
model: filteredModel
visible: !filePickerPanel.viewMode
gradientColor: Color.mSurface
delegate: Rectangle {
id: listItem
+100 -24
View File
@@ -6,15 +6,8 @@ import qs.Commons
Item {
id: root
// Intercept all key events at the root level to prevent GridView from handling them
Keys.onPressed: event => {
// Don't let this event reach the GridView
event.accepted = false;
}
Keys.onReleased: event => {
event.accepted = false;
}
// Signal for key press events when keyNavigationEnabled is true
signal keyPressed(var event)
property color handleColor: Qt.alpha(Color.mHover, 0.8)
property color handleHoverColor: handleColor
@@ -30,6 +23,19 @@ Item {
return gridView.contentHeight > gridView.height;
}
// Gradient properties
property bool showGradientMasks: true
property color gradientColor: Color.mSurfaceVariant
property int gradientHeight: 16
property bool reserveScrollbarSpace: true
// Available width for content (excludes scrollbar space when reserveScrollbarSpace is true)
// Note: Always reserves space when enabled to avoid binding loops with cellWidth calculations
readonly property real availableWidth: width - (reserveScrollbarSpace ? handleWidth + Style.marginXS : 0)
// Expose activeFocus from internal gridView
readonly property bool hasActiveFocus: gridView.activeFocus
// Forward GridView properties
property alias model: gridView.model
property alias delegate: gridView.delegate
@@ -68,6 +74,7 @@ Item {
property alias dragging: gridView.dragging
property alias horizontalVelocity: gridView.horizontalVelocity
property alias verticalVelocity: gridView.verticalVelocity
property alias reuseItems: gridView.reuseItems
// Forward GridView methods
function positionViewAtIndex(index, mode) {
@@ -86,6 +93,10 @@ Item {
gridView.forceLayout();
}
function forceActiveFocus() {
gridView.forceActiveFocus();
}
function cancelFlick() {
gridView.cancelFlick();
}
@@ -114,13 +125,83 @@ Item {
return gridView.itemAtIndex(index);
}
function moveCurrentIndexUp() {
gridView.moveCurrentIndexUp();
}
function moveCurrentIndexDown() {
gridView.moveCurrentIndexDown();
}
function moveCurrentIndexLeft() {
gridView.moveCurrentIndexLeft();
}
function moveCurrentIndexRight() {
gridView.moveCurrentIndexRight();
}
// Set reasonable implicit sizes for Layout usage
implicitWidth: 200
implicitHeight: 200
Component.onCompleted: {
createGradients();
}
// Dynamically create gradient overlays
function createGradients() {
if (!showGradientMasks)
return;
Qt.createQmlObject(`
import QtQuick
import qs.Commons
Rectangle {
x: 0
y: 0
width: root.availableWidth
height: root.gradientHeight
z: 1
visible: root.showGradientMasks && root.verticalScrollBarActive
opacity: gridView.contentY <= 1 ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: root.gradientColor }
GradientStop { position: 1.0; color: "transparent" }
}
}
`, root, "topGradient");
Qt.createQmlObject(`
import QtQuick
import qs.Commons
Rectangle {
x: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: -1
width: root.availableWidth
height: root.gradientHeight + 1
z: 1
visible: root.showGradientMasks && root.verticalScrollBarActive
opacity: (gridView.contentY + gridView.height >= gridView.contentHeight - 1) ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 1.0; color: root.gradientColor }
}
}
`, root, "bottomGradient");
}
GridView {
id: gridView
anchors.fill: parent
anchors.rightMargin: root.reserveScrollbarSpace ? root.handleWidth + Style.marginXS : 0
// Enable clipping to keep content within bounds
clip: true
@@ -128,27 +209,22 @@ Item {
// Enable flickable for smooth scrolling
boundsBehavior: Flickable.StopAtBounds
// Completely disable focus to prevent any keyboard interaction
focus: false
activeFocusOnTab: false
enabled: true // Still enabled for mouse interaction
// Focus handling depends on keyNavigationEnabled
focus: keyNavigationEnabled
activeFocusOnTab: keyNavigationEnabled
// Override key navigation - do nothing
// Emit keyPressed signal for custom key handling
Keys.onPressed: event => {
// Consume the event here so GridView doesn't process it
// but don't actually do anything
event.accepted = true;
if (keyNavigationEnabled) {
root.keyPressed(event);
}
}
Keys.onReleased: event => {
event.accepted = true;
}
ScrollBar.vertical: ScrollBar {
parent: gridView
x: gridView.mirrored ? 0 : gridView.width - width
parent: root
x: root.mirrored ? 0 : root.width - width
y: 0
height: gridView.height
height: root.height
policy: root.verticalPolicy
contentItem: Rectangle {
+65 -3
View File
@@ -20,6 +20,14 @@ Item {
return listView.contentHeight > listView.height;
}
property bool showGradientMasks: true
property color gradientColor: Color.mSurfaceVariant
property int gradientHeight: 16
property bool reserveScrollbarSpace: true
// Available width for content (excludes scrollbar space when reserveScrollbarSpace is true)
readonly property real availableWidth: width - (reserveScrollbarSpace ? handleWidth + Style.marginXS : 0)
// Forward ListView properties
property alias model: listView.model
property alias delegate: listView.delegate
@@ -108,18 +116,72 @@ Item {
implicitWidth: 200
implicitHeight: 200
Component.onCompleted: {
createGradients();
}
// Dynamically create gradient overlays
function createGradients() {
if (!showGradientMasks)
return;
Qt.createQmlObject(`
import QtQuick
import qs.Commons
Rectangle {
x: 0
y: 0
width: root.availableWidth
height: root.gradientHeight
z: 1
visible: root.showGradientMasks && root.verticalScrollBarActive
opacity: listView.contentY <= 1 ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: root.gradientColor }
GradientStop { position: 1.0; color: "transparent" }
}
}
`, root, "topGradient");
Qt.createQmlObject(`
import QtQuick
import qs.Commons
Rectangle {
x: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: -1
width: root.availableWidth
height: root.gradientHeight + 1
z: 1
visible: root.showGradientMasks && root.verticalScrollBarActive
opacity: (listView.contentY + listView.height >= listView.contentHeight - 1) ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 1.0; color: root.gradientColor }
}
}
`, root, "bottomGradient");
}
ListView {
id: listView
anchors.fill: parent
anchors.rightMargin: root.reserveScrollbarSpace ? root.handleWidth + Style.marginXS : 0
clip: true
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
parent: listView
x: listView.mirrored ? 0 : listView.width - width
parent: root
x: root.mirrored ? 0 : root.width - width
y: 0
height: listView.height
height: root.height
policy: root.verticalPolicy
contentItem: Rectangle {
+56
View File
@@ -18,6 +18,13 @@ T.ScrollView {
property int boundsBehavior: Flickable.StopAtBounds
readonly property bool verticalScrollable: contentItem.contentHeight > contentItem.height
readonly property bool horizontalScrollable: contentItem.contentWidth > contentItem.width
property bool showGradientMasks: true
property color gradientColor: Color.mSurfaceVariant
property int gradientHeight: 16
property bool reserveScrollbarSpace: true
property real userRightPadding: 0
rightPadding: userRightPadding + (reserveScrollbarSpace && verticalScrollable ? handleWidth + Style.marginXS : 0)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
@@ -25,6 +32,55 @@ T.ScrollView {
// Configure the internal flickable when it becomes available
Component.onCompleted: {
configureFlickable();
createGradients();
}
// Dynamically create gradient overlays to avoid interfering with ScrollView content management
function createGradients() {
if (!showGradientMasks)
return;
Qt.createQmlObject(`
import QtQuick
import qs.Commons
Rectangle {
x: root.leftPadding
y: root.topPadding
width: root.availableWidth
height: root.gradientHeight
z: 1
visible: root.showGradientMasks && root.verticalScrollable
opacity: root.contentItem.contentY <= 1 ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: root.gradientColor }
GradientStop { position: 1.0; color: "transparent" }
}
}
`, root, "topGradient");
Qt.createQmlObject(`
import QtQuick
import qs.Commons
Rectangle {
x: root.leftPadding
y: root.height - root.bottomPadding - height + 1
width: root.availableWidth
height: root.gradientHeight + 1
z: 1
visible: root.showGradientMasks && root.verticalScrollable
opacity: (root.contentItem.contentY + root.contentItem.height >= root.contentItem.contentHeight - 1) ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad }
}
gradient: Gradient {
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 1.0; color: root.gradientColor }
}
}
`, root, "bottomGradient");
}
// Function to configure the underlying Flickable