import QtQuick // Небольшое полотно графа связей: узлы-файлы (синие) и теги (зелёные), // рёбра — связи; узлы можно перетаскивать, клик по файлу открывает его. Canvas { id: g property var nodes: [] // [{key,name,type}] property var edges: [] // [{from,to}] signal fileClicked(string path) property var pos: ({}) // key -> {x,y} property string dragKey: "" function nodeForKey(key) { for (let i = 0; i < nodes.length; ++i) if (nodes[i].key === key) return nodes[i] return null } function relayout() { pos = {} const n = nodes.length if (n === 0) return const cx = width / 2, cy = height / 2 const rad = Math.min(width, height) / 2 - 30 for (let i = 0; i < n; ++i) { const a = (i / n) * 2 * Math.PI - Math.PI / 2 pos[nodes[i].key] = { x: cx + rad * Math.cos(a), y: cy + rad * Math.sin(a) } } requestPaint() } onNodesChanged: relayout() onWidthChanged: relayout() onHeightChanged: relayout() onPaint: { const ctx = getContext("2d") ctx.clearRect(0, 0, width, height) if (nodes.length === 0) return // рёбра ctx.lineWidth = 1 ctx.strokeStyle = "#b9c6d6" for (let i = 0; i < edges.length; ++i) { const f = pos[edges[i].from] const t = pos[edges[i].to] if (!f || !t) continue ctx.beginPath() ctx.moveTo(f.x, f.y) ctx.lineTo(t.x, t.y) ctx.stroke() } // узлы ctx.textAlign = "center" for (let i = 0; i < nodes.length; ++i) { const nd = nodes[i] const p = pos[nd.key] if (!p) continue const isFile = nd.type === "file" const r = isFile ? 6 : 5 ctx.beginPath() ctx.arc(p.x, p.y, r, 0, 2 * Math.PI) ctx.fillStyle = isFile ? (nd.key === g.dragKey ? "#1a6ce0" : "#3f7fd0") : "#3aa06a" ctx.fill() ctx.strokeStyle = "#ffffff" ctx.lineWidth = 1 ctx.stroke() ctx.fillStyle = "#5a6a7d" ctx.font = "9px sans-serif" const label = isFile ? nd.name : nd.name const txt = label.length > 14 ? label.substring(0, 13) + "…" : label ctx.fillText(txt, p.x, p.y + r + 10) } } MouseArea { anchors.fill: parent onPressed: (m) => { dragKey = "" for (const key in g.pos) { const p = g.pos[key] const dx = m.x - p.x, dy = m.y - p.y if (dx * dx + dy * dy <= 144) { // радиус захвата 12px dragKey = key break } } } onPositionChanged: (m) => { if (dragKey.length > 0 && g.pos[dragKey]) { g.pos[dragKey] = { x: m.x, y: m.y } g.requestPaint() } } onReleased: (m) => { if (dragKey.length === 0) { for (const key in g.pos) { const p = g.pos[key] const dx = m.x - p.x, dy = m.y - p.y if (dx * dx + dy * dy <= 100) { const nd = g.nodeForKey(key) if (nd && nd.type === "file") g.fileClicked(nd.key) break } } } dragKey = "" } } }