gui: session tree with Active/Paused groups, arbitrary-depth expand/collapse
This commit is contained in:
parent
265a1fdda9
commit
22302ce9f1
105
gui/main.qml
105
gui/main.qml
|
|
@ -118,8 +118,37 @@ ApplicationWindow {
|
||||||
sessionTreeModel.clear()
|
sessionTreeModel.clear()
|
||||||
modelCache = {}
|
modelCache = {}
|
||||||
var sessions = ollie.sessions
|
var sessions = ollie.sessions
|
||||||
|
|
||||||
|
// Sessions are already sorted: active first, paused last.
|
||||||
|
// Insert group headers when paused state changes.
|
||||||
|
var lastPaused = null
|
||||||
for (var i = 0; i < sessions.length; ++i) {
|
for (var i = 0; i < sessions.length; ++i) {
|
||||||
var sess = sessions[i]
|
var sess = sessions[i]
|
||||||
|
var isPaused = sess.paused || false
|
||||||
|
|
||||||
|
// Insert header on transition
|
||||||
|
if (lastPaused === null && !isPaused) {
|
||||||
|
sessionTreeModel.append({
|
||||||
|
type: "header",
|
||||||
|
level: 0,
|
||||||
|
displayName: "Active"
|
||||||
|
})
|
||||||
|
} else if (lastPaused === false && isPaused) {
|
||||||
|
sessionTreeModel.append({
|
||||||
|
type: "header",
|
||||||
|
level: 0,
|
||||||
|
displayName: "Inactive"
|
||||||
|
})
|
||||||
|
} else if (lastPaused === null && isPaused) {
|
||||||
|
// All sessions are paused - show Inactive header
|
||||||
|
sessionTreeModel.append({
|
||||||
|
type: "header",
|
||||||
|
level: 0,
|
||||||
|
displayName: "Inactive"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
lastPaused = isPaused
|
||||||
|
|
||||||
appendSessionToModel(sess)
|
appendSessionToModel(sess)
|
||||||
}
|
}
|
||||||
if (hadFocus)
|
if (hadFocus)
|
||||||
|
|
@ -168,7 +197,8 @@ ApplicationWindow {
|
||||||
level: 1,
|
level: 1,
|
||||||
state: agent.state || "idle",
|
state: agent.state || "idle",
|
||||||
connected: agent.connected,
|
connected: agent.connected,
|
||||||
displayName: agent.name
|
displayName: agent.name,
|
||||||
|
parentExpanded: wasExpanded
|
||||||
})
|
})
|
||||||
agentIds.push(agent.id)
|
agentIds.push(agent.id)
|
||||||
}
|
}
|
||||||
|
|
@ -319,11 +349,41 @@ ApplicationWindow {
|
||||||
|
|
||||||
function toggleExpand(index) {
|
function toggleExpand(index) {
|
||||||
var item = sessionTreeModel.get(index)
|
var item = sessionTreeModel.get(index)
|
||||||
if (item.type === "session") {
|
if (!item.expanded === undefined) return // only expandable items
|
||||||
var newExpanded = !item.expanded
|
|
||||||
sessionTreeModel.set(index, { expanded: newExpanded })
|
var newExpanded = !item.expanded
|
||||||
|
var itemLevel = item.level || 0
|
||||||
|
sessionTreeModel.setProperty(index, "expanded", newExpanded)
|
||||||
|
|
||||||
|
// Cache for sessions (by id)
|
||||||
|
if (item.type === "session" && item.id) {
|
||||||
sessionExpandedCache[item.id] = newExpanded
|
sessionExpandedCache[item.id] = newExpanded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update all descendants: items with level > itemLevel until we hit same or lower level
|
||||||
|
for (var i = index + 1; i < sessionTreeModel.count; ++i) {
|
||||||
|
var child = sessionTreeModel.get(i)
|
||||||
|
var childLevel = child.level || 0
|
||||||
|
if (childLevel <= itemLevel) break // end of descendants
|
||||||
|
|
||||||
|
// Determine visibility: walk up to find if any ancestor is collapsed
|
||||||
|
var visible = newExpanded
|
||||||
|
if (visible) {
|
||||||
|
// Check all ancestors between this item and the child
|
||||||
|
for (var j = i - 1; j >= index; --j) {
|
||||||
|
var ancestor = sessionTreeModel.get(j)
|
||||||
|
var ancestorLevel = ancestor.level || 0
|
||||||
|
if (ancestorLevel < childLevel && ancestor.expanded === false) {
|
||||||
|
visible = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (ancestorLevel < childLevel) {
|
||||||
|
childLevel = ancestorLevel // track the level we've checked up to
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sessionTreeModel.setProperty(i, "parentExpanded", visible)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateAgent(parentId, agentId) {
|
function activateAgent(parentId, agentId) {
|
||||||
|
|
@ -378,12 +438,8 @@ ApplicationWindow {
|
||||||
Connections {
|
Connections {
|
||||||
target: ollie
|
target: ollie
|
||||||
function onSessionsChanged() {
|
function onSessionsChanged() {
|
||||||
// Use incremental update if model exists, full rebuild otherwise
|
// Always do full rebuild since headers complicate incremental updates
|
||||||
if (sessionTreeModel.count > 0 && Object.keys(modelCache).length > 0) {
|
buildSessionTree()
|
||||||
updateSessionTree()
|
|
||||||
} else {
|
|
||||||
buildSessionTree()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function onAgentStateChanged(sessionId, agentId, state) {
|
function onAgentStateChanged(sessionId, agentId, state) {
|
||||||
// Update only the specific agent in the model
|
// Update only the specific agent in the model
|
||||||
|
|
@ -889,17 +945,23 @@ ApplicationWindow {
|
||||||
// Guard against undefined during initial render
|
// Guard against undefined during initial render
|
||||||
height: {
|
height: {
|
||||||
if (!type) return 0
|
if (!type) return 0
|
||||||
if (type === "agent" && !parentSessionExpanded(parentId))
|
if (type === "header") return 24
|
||||||
|
if (type === "agent" && !parentExpanded)
|
||||||
return 0
|
return 0
|
||||||
return 28
|
return 28
|
||||||
}
|
}
|
||||||
visible: height > 0
|
visible: height > 0
|
||||||
property int modelIndex: index
|
property int modelIndex: index
|
||||||
|
property bool isHeader: type === "header"
|
||||||
|
|
||||||
|
// Headers are not interactive
|
||||||
|
enabled: !isHeader
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
hoverEnabled: false
|
hoverEnabled: false
|
||||||
|
enabled: !parent.isHeader
|
||||||
onClicked: function(mouse) {
|
onClicked: function(mouse) {
|
||||||
if (mouse.button === Qt.RightButton) {
|
if (mouse.button === Qt.RightButton) {
|
||||||
if (type === "session") {
|
if (type === "session") {
|
||||||
|
|
@ -995,8 +1057,8 @@ ApplicationWindow {
|
||||||
|
|
||||||
// Indent + expand/collapse indicator
|
// Indent + expand/collapse indicator
|
||||||
Item {
|
Item {
|
||||||
Layout.preferredWidth: (level || 0) * 16 + 16
|
Layout.preferredWidth: type === "header" ? 8 : (level || 0) * 16 + 16
|
||||||
Layout.preferredHeight: 28
|
Layout.preferredHeight: type === "header" ? 24 : 28
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
@ -1004,13 +1066,14 @@ ApplicationWindow {
|
||||||
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10
|
||||||
color: theme.textColor
|
color: theme.textColor
|
||||||
opacity: 0.6
|
opacity: 0.6
|
||||||
|
visible: type !== "header"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name area
|
// Name area
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 28
|
Layout.preferredHeight: type === "header" ? 24 : 28
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -1043,20 +1106,23 @@ ApplicationWindow {
|
||||||
Label {
|
Label {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
text: displayName || ""
|
text: displayName || ""
|
||||||
color: theme.textColor
|
color: type === "header" ? theme.midColor : theme.textColor
|
||||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * (type === "header" ? 0.85 : 1) : Qt.application.font.pointSize
|
||||||
|
font.bold: type === "header"
|
||||||
|
font.capitalization: type === "header" ? Font.AllUppercase : Font.MixedCase
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
visible: modelIndex !== renamingIndex
|
visible: modelIndex !== renamingIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// State dot
|
// State dot (hidden for headers)
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: stateDot
|
id: stateDot
|
||||||
Layout.preferredWidth: 8
|
Layout.preferredWidth: 8
|
||||||
Layout.preferredHeight: 8
|
Layout.preferredHeight: 8
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
radius: 4
|
radius: 4
|
||||||
|
visible: type !== "header"
|
||||||
color: {
|
color: {
|
||||||
if (type === "session") {
|
if (type === "session") {
|
||||||
// Session: RPC connection health
|
// Session: RPC connection health
|
||||||
|
|
@ -1076,11 +1142,6 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
highlighted: (type === "agent" && agentId === ollie.activeAgentId && parentId === ollie.activeSessionId)
|
highlighted: (type === "agent" && agentId === ollie.activeAgentId && parentId === ollie.activeSessionId)
|
||||||
|
|
||||||
function parentSessionExpanded(pid) {
|
|
||||||
// O(1) lookup using cache instead of O(n) model scan
|
|
||||||
return sessionExpandedCache[pid] !== undefined ? sessionExpandedCache[pid] : true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in New Issue