Skip to content

Commit 96ad66d

Browse files
committed
refactor(qml): data-driven sidebar sections and self-contained GtkScrollBar
- Refactor Sidebar.qml to use sectionsModel repeater (GRID/LIST types) - Add SidebarHeader.qml with collapse/expand and dynamic action buttons - Add title/icon Properties to QuickAccessBridge and VolumesBridge - Selection tracking by path instead of name (currentSelectionPath) - GtkScrollBar: add physics engine (acceleration, rubber-band, snap-back) - GtkScrollBar: add optional background track, self-contained visibility logic - Move scroll physics from JustifiedView into GtkScrollBar.handleWheel() - Use font.pointSize instead of pixelSize for DPI-aware text
1 parent 31b00a2 commit 96ad66d

File tree

7 files changed

+505
-121
lines changed

7 files changed

+505
-121
lines changed

core/gio_bridge/quick_access.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import gi
22
gi.require_version('Gio', '2.0')
33
from gi.repository import GLib, Gio
4-
from PySide6.QtCore import QObject, Signal, Slot
4+
from PySide6.QtCore import QObject, Signal, Slot, Property
55
from pathlib import Path
66

77
# Import the reactive bookmarks bridge
@@ -13,6 +13,9 @@ class QuickAccessBridge(QObject):
1313
"""
1414
itemsChanged = Signal()
1515

16+
displayTitle = "Quick Access"
17+
displayIcon = "star"
18+
1619
def __init__(self, parent=None):
1720
super().__init__(parent)
1821
self._bookmarks_bridge = BookmarksBridge(self)
@@ -23,6 +26,14 @@ def __init__(self, parent=None):
2326
self._trash_monitor = None
2427
self._setup_trash_monitor()
2528

29+
@Property(str, constant=True)
30+
def title(self):
31+
return self.displayTitle
32+
33+
@Property(str, constant=True)
34+
def icon(self):
35+
return self.displayIcon
36+
2637
def _setup_trash_monitor(self):
2738
try:
2839
self._trash_file = Gio.File.new_for_uri("trash:///")

core/gio_bridge/volumes.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
gi.require_version('Gio', '2.0')
1616
from gi.repository import Gio, GLib
1717

18-
from PySide6.QtCore import QObject, Signal, Slot
18+
from PySide6.QtCore import QObject, Signal, Slot, Property
1919

2020
class VolumesBridge(QObject):
2121
"""
@@ -27,6 +27,9 @@ class VolumesBridge(QObject):
2727
mountSuccess = Signal(str) # identifier
2828
mountError = Signal(str) # error message
2929

30+
displayTitle = "Devices"
31+
displayIcon = "hard_drive"
32+
3033
def __init__(self, parent=None):
3134
super().__init__(parent)
3235
self.monitor = Gio.VolumeMonitor.get()
@@ -44,6 +47,14 @@ def _on_monitor_changed(self, monitor, item):
4447
"""Pass-through signal when anything changes in Gioland."""
4548
self.volumesChanged.emit()
4649

50+
@Property(str, constant=True)
51+
def title(self):
52+
return self.displayTitle
53+
54+
@Property(str, constant=True)
55+
def icon(self):
56+
return self.displayIcon
57+
4758
def _get_icon_name(self, gicon):
4859
"""Extract the best icon name from a GIcon."""
4960
if not gicon:

ui/qml/components/GtkScrollBar.qml

Lines changed: 142 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,170 @@ import QtQuick.Controls
77
* Features:
88
* - Expanding thumb (4.5px -> 9px) on hover.
99
* - Auto-hides after 2 seconds of inactivity.
10-
* - Reappears on mouse movement over target, scrolling, or focus activity.
10+
* - Optional "pillar" track (showTrack: true).
11+
* - Self-contained geometry logic + optional external flickable binding.
1112
*/
1213
ScrollBar {
1314
id: control
1415

15-
// The Flickable (ListView/GridView) this scrollbar controls
16-
property Flickable flickable: parent
16+
// --- CONFIGURATION ---
17+
// Optional: Flickable binding for manual usage (triggers activity)
18+
property Flickable flickable: null
19+
// Optional: Show a subtle background track when active
20+
property bool showTrack: false
1721

22+
// --- PHYSICS CONFIGURATION ---
23+
// Toggle internal physics engine
24+
property bool physicsEnabled: false
25+
// Enable "Turbo" acceleration curve (faster ramp-up)
26+
property bool turboMode: true
27+
// Current acceleration multiplier
28+
property real acceleration: 1.0
29+
30+
// Internal State for Physics
31+
property real lastWheelTime: 0
32+
property int lastDeltaSign: 0
33+
34+
// Physics Timer for "SnapBack" (overshoot recovery)
35+
Timer {
36+
id: snapBackTimer
37+
interval: 150
38+
onTriggered: {
39+
if (!control.flickable) return
40+
// Dynamic Bounds: Respect margins (paddings)
41+
let minY = -control.flickable.topMargin
42+
let maxY = Math.max(minY, control.flickable.contentHeight - control.flickable.height + control.flickable.bottomMargin)
43+
44+
if (control.flickable.contentY < minY) {
45+
control.flickable.contentY = minY
46+
} else if (control.flickable.contentY > maxY) {
47+
control.flickable.contentY = maxY
48+
}
49+
}
50+
}
51+
52+
// --- PHYSICS ENGINE ---
53+
54+
// Public API: Call this from parent's WheelHandler
55+
function handleWheel(event) {
56+
if (!control.physicsEnabled || !control.flickable) return
57+
58+
// Dynamic Bounds
59+
let minY = -control.flickable.topMargin
60+
let maxY = Math.max(minY, control.flickable.contentHeight - control.flickable.height + control.flickable.bottomMargin)
61+
62+
if (maxY < 20) return
63+
64+
// 1. Acceleration Logic
65+
let now = new Date().getTime()
66+
let dt = now - control.lastWheelTime
67+
control.lastWheelTime = now
68+
69+
let currentSign = (event.angleDelta.y > 0) ? 1 : -1
70+
71+
if (dt < 100 && currentSign === control.lastDeltaSign && event.angleDelta.y !== 0) {
72+
let ramp = control.turboMode ? 1.0 : 0.5
73+
let limit = control.turboMode ? 10.0 : 6.0
74+
control.acceleration = Math.min(control.acceleration + ramp, limit)
75+
} else {
76+
control.acceleration = 1.0
77+
}
78+
control.lastDeltaSign = currentSign
79+
80+
// 2. Calculate Delta
81+
let delta = 0
82+
if (event.angleDelta.y !== 0) {
83+
delta = -(event.angleDelta.y / 1.2)
84+
delta *= control.acceleration
85+
} else if (event.pixelDelta.y !== 0) {
86+
delta = -event.pixelDelta.y
87+
}
88+
89+
if (delta === 0) return
90+
91+
// 3. Resistance (Rubber Banding)
92+
if (control.flickable.contentY < minY || control.flickable.contentY > maxY) {
93+
delta *= 0.3
94+
}
95+
96+
// 4. Apply with Clamped Overshoot Limits
97+
let newY = control.flickable.contentY + delta
98+
if (newY < minY - 300) newY = minY - 300
99+
if (newY > maxY + 300) newY = maxY + 300
100+
101+
control.flickable.contentY = newY
102+
103+
snapBackTimer.restart()
104+
activityTimer.restart()
105+
}
106+
107+
// fallback: Internal handler (only works if hovering scrollbar directly)
108+
// We keep this for cases where user HOVERS the scrollbar itself
109+
WheelHandler {
110+
target: null // Logic handled via function call
111+
enabled: control.physicsEnabled
112+
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
113+
onWheel: (event) => control.handleWheel(event)
114+
}
115+
116+
// --- LOGIC ---
117+
// Active when hovered, pressed, or recently used (timer)
18118
active: hovered || pressed || activityTimer.running
119+
120+
// Only render when content is scrollable (size < 1.0) and valid
121+
visible: control.size < 1.0 && control.size > 0
122+
19123
interactive: true
20124
hoverEnabled: true
21-
22-
// Track: Transparent
23-
background: null
24125

25126
SystemPalette { id: activePalette; colorGroup: SystemPalette.Active }
26127

27-
// Logic: Auto-hide timer
128+
// Auto-hide Timer
28129
Timer {
29130
id: activityTimer
30131
interval: 2000
31132
onTriggered: stop()
32133
}
33134

34-
// Activity Trigger: Scrolling
135+
// --- ACTIVITY TRIGGERS ---
136+
// 1. Internal Geometry Changes (Works universally)
137+
onPositionChanged: activityTimer.restart()
138+
onSizeChanged: activityTimer.restart()
139+
140+
// 2. Mouse Interaction
141+
onHoveredChanged: if (hovered) activityTimer.restart()
142+
143+
// 3. External Flickable Signals (if bound)
35144
Connections {
36145
target: flickable
37146
function onContentYChanged() { activityTimer.restart() }
38147
function onContentXChanged() { activityTimer.restart() }
148+
function onMovementStarted() { activityTimer.restart() }
149+
ignoreUnknownSignals: true
39150
}
40-
41-
// Activity Trigger: Mouse Movement over the target area
42-
HoverHandler {
43-
target: flickable
44-
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
45-
onPointChanged: activityTimer.restart()
151+
152+
// 4. Parent View Interactivity (if parent is Flickable/ListView)
153+
// This handles the "unhide on scroll/hover" feature requested
154+
Connections {
155+
target: control.parent && control.parent.flickableItem ? control.parent.flickableItem : (control.parent instanceof Flickable ? control.parent : null)
156+
function onMovementStarted() { activityTimer.restart() }
157+
ignoreUnknownSignals: true
46158
}
47159

48-
// Activity Trigger: Focus/Key events (when attached to a focused item)
49-
Connections {
50-
target: flickable
51-
function onActiveFocusChanged() { if (flickable.activeFocus) activityTimer.restart() }
160+
// --- VISUALS ---
161+
162+
// Background Track (Pillar) - Optional
163+
background: Rectangle {
164+
visible: control.showTrack && control.active
165+
// Ensure track fills the scrollbar area
166+
anchors.fill: parent
167+
// Slightly more visible color for testing/usage
168+
color: Qt.alpha(activePalette.windowText, 0.1)
169+
opacity: control.active ? 1.0 : 0.0
170+
Behavior on opacity { NumberAnimation { duration: 200 } }
52171
}
53172

54-
// VISUALS
173+
// Thumb
55174
contentItem: Rectangle {
56175
implicitWidth: control.hovered || control.pressed ? 9 : 4.5
57176
radius: width / 2
@@ -61,8 +180,12 @@ ScrollBar {
61180
control.hovered ? Qt.alpha(activePalette.text, 0.7) :
62181
Qt.alpha(activePalette.text, 0.4)
63182

183+
// Fade in/out
184+
opacity: control.active ? 1.0 : 0.0
185+
64186
// Animations: Smooth GTK feel
65187
Behavior on implicitWidth { NumberAnimation { duration: 100 } }
66188
Behavior on color { ColorAnimation { duration: 150 } }
189+
Behavior on opacity { NumberAnimation { duration: 200 } }
67190
}
68191
}

0 commit comments

Comments
 (0)