2026-08-17 14:18:43 +03:00
|
|
|
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: ""
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
property real zoom: 1.0
|
|
|
|
|
property real panX: 0
|
|
|
|
|
property real panY: 0
|
2026-08-17 16:04:46 +03:00
|
|
|
property real lastMouseX: 0
|
|
|
|
|
property real lastMouseY: 0
|
2026-08-17 14:18:43 +03:00
|
|
|
|
|
|
|
|
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
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
if (n === 0) { requestPaint(); return }
|
2026-08-17 14:18:43 +03:00
|
|
|
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) }
|
|
|
|
|
}
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
zoom = 1.0; panX = 0; panY = 0
|
2026-08-17 14:18:43 +03:00
|
|
|
requestPaint()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onNodesChanged: relayout()
|
|
|
|
|
onWidthChanged: relayout()
|
|
|
|
|
onHeightChanged: relayout()
|
|
|
|
|
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
function paintCanvas() {
|
2026-08-17 14:18:43 +03:00
|
|
|
const ctx = getContext("2d")
|
|
|
|
|
ctx.clearRect(0, 0, width, height)
|
|
|
|
|
if (nodes.length === 0) return
|
|
|
|
|
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
ctx.save()
|
|
|
|
|
ctx.translate(width / 2 + panX, height / 2 + panY)
|
|
|
|
|
ctx.scale(zoom, zoom)
|
|
|
|
|
ctx.translate(-width / 2, -height / 2)
|
|
|
|
|
|
2026-08-17 14:18:43 +03:00
|
|
|
// рёбра
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
ctx.lineWidth = 1 / zoom
|
2026-08-17 14:18:43 +03:00
|
|
|
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"
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
ctx.font = (9 / zoom) + "px sans-serif"
|
|
|
|
|
const txt = nd.name.length > 14 ? nd.name.substring(0, 13) + "…" : nd.name
|
|
|
|
|
ctx.fillText(txt, p.x, p.y + r + 10 / zoom)
|
2026-08-17 14:18:43 +03:00
|
|
|
}
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
|
|
|
|
|
ctx.restore()
|
2026-08-17 14:18:43 +03:00
|
|
|
}
|
|
|
|
|
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
onPaint: paintCanvas()
|
|
|
|
|
|
2026-08-17 14:18:43 +03:00
|
|
|
MouseArea {
|
|
|
|
|
anchors.fill: parent
|
2026-08-17 16:04:46 +03:00
|
|
|
acceptedButtons: Qt.LeftButton
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
onWheel: (w) => {
|
|
|
|
|
const factor = w.angleDelta.y > 0 ? 1.15 : 0.87
|
|
|
|
|
g.zoom = Math.max(0.2, Math.min(5.0, g.zoom * factor))
|
|
|
|
|
g.requestPaint()
|
|
|
|
|
}
|
2026-08-17 14:18:43 +03:00
|
|
|
onPressed: (m) => {
|
2026-08-17 16:04:46 +03:00
|
|
|
g.dragKey = ""
|
|
|
|
|
g.lastMouseX = m.x
|
|
|
|
|
g.lastMouseY = m.y
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
const gx = (m.x - width / 2 - panX) / zoom + width / 2
|
|
|
|
|
const gy = (m.y - height / 2 - panY) / zoom + height / 2
|
2026-08-17 14:18:43 +03:00
|
|
|
for (const key in g.pos) {
|
|
|
|
|
const p = g.pos[key]
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
const dx = gx - p.x, dy = gy - p.y
|
2026-08-17 16:04:46 +03:00
|
|
|
if (dx * dx + dy * dy <= 256) {
|
|
|
|
|
g.dragKey = key
|
2026-08-17 14:18:43 +03:00
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
onPositionChanged: (m) => {
|
2026-08-17 16:04:46 +03:00
|
|
|
if (g.dragKey.length > 0 && g.pos[g.dragKey]) {
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
const gx = (m.x - width / 2 - panX) / zoom + width / 2
|
|
|
|
|
const gy = (m.y - height / 2 - panY) / zoom + height / 2
|
2026-08-17 16:04:46 +03:00
|
|
|
g.pos[g.dragKey] = { x: gx, y: gy }
|
|
|
|
|
} else {
|
|
|
|
|
g.panX += m.x - g.lastMouseX
|
|
|
|
|
g.panY += m.y - g.lastMouseY
|
2026-08-17 14:18:43 +03:00
|
|
|
}
|
2026-08-17 16:04:46 +03:00
|
|
|
g.lastMouseX = m.x
|
|
|
|
|
g.lastMouseY = m.y
|
|
|
|
|
g.requestPaint()
|
2026-08-17 14:18:43 +03:00
|
|
|
}
|
|
|
|
|
onReleased: (m) => {
|
2026-08-17 16:04:46 +03:00
|
|
|
if (g.dragKey.length === 0) {
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
const gx = (m.x - width / 2 - panX) / zoom + width / 2
|
|
|
|
|
const gy = (m.y - height / 2 - panY) / zoom + height / 2
|
2026-08-17 14:18:43 +03:00
|
|
|
for (const key in g.pos) {
|
|
|
|
|
const p = g.pos[key]
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
const dx = gx - p.x, dy = gy - p.y
|
|
|
|
|
if (dx * dx + dy * dy <= 144) {
|
2026-08-17 14:18:43 +03:00
|
|
|
const nd = g.nodeForKey(key)
|
|
|
|
|
if (nd && nd.type === "file")
|
|
|
|
|
g.fileClicked(nd.key)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-17 16:04:46 +03:00
|
|
|
g.dragKey = ""
|
2026-08-17 14:18:43 +03:00
|
|
|
}
|
2026-08-17 16:04:46 +03:00
|
|
|
onDoubleClicked: {
|
Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
2026-08-17 15:42:56 +03:00
|
|
|
g.zoom = 1.0; g.panX = 0; g.panY = 0; g.relayout()
|
|
|
|
|
}
|
2026-08-17 14:18:43 +03:00
|
|
|
}
|
|
|
|
|
}
|