diff --git a/qml/panels/EditorPane.qml b/qml/panels/EditorPane.qml index e0a419b..2cc626e 100644 --- a/qml/panels/EditorPane.qml +++ b/qml/panels/EditorPane.qml @@ -312,6 +312,10 @@ Item { onDocIndexChanged: { pane.refreshHeader() pane.renderView() + if (pane.docIndex >= 0 && pane.editMode) { + editor.text = documentManager.contentAt(pane.docIndex) + editor.forceActiveFocus() + } } onEditModeChanged: { if (!pane.editMode) diff --git a/qml/panels/EditorPanel.qml b/qml/panels/EditorPanel.qml index 0568254..5f0a0cf 100644 --- a/qml/panels/EditorPanel.qml +++ b/qml/panels/EditorPanel.qml @@ -47,14 +47,20 @@ Item { anchors.rightMargin: 2 spacing: 4 - Label { - id: tabLabel + MouseArea { Layout.fillWidth: true - elide: Text.ElideMiddle - text: root.baseName(modelData) - + (documentManager.isDirty(index) ? " •" : "") - font.pixelSize: 12 - color: active ? "#111" : "#555" + Layout.fillHeight: true + onClicked: documentManager.setActiveIndex(index) + onDoubleClicked: documentManager.closeTab(index) + + Label { + id: tabLabel + anchors.verticalCenter: parent.verticalCenter + text: root.baseName(tab.modelData) + + (documentManager.isDirty(tab.index) ? " •" : "") + font.pixelSize: 12 + color: tab.active ? "#111" : "#555" + } } ToolButton { @@ -63,15 +69,9 @@ Item { implicitHeight: 20 font.pixelSize: 9 opacity: tab.active || hovered ? 1 : 0 - onClicked: documentManager.closeTab(index) + onClicked: documentManager.closeTab(tab.index) } } - - MouseArea { - anchors.fill: parent - onClicked: documentManager.setActiveIndex(index) - onDoubleClicked: documentManager.closeTab(index) - } } } diff --git a/qml/panels/FileTreePanel.qml b/qml/panels/FileTreePanel.qml index 94c67e8..d21d8c9 100644 --- a/qml/panels/FileTreePanel.qml +++ b/qml/panels/FileTreePanel.qml @@ -302,6 +302,30 @@ Item { } } } + MenuItem { + text: qsTr("Создать файл внутри") + visible: rowMenu.targetIsDir + onClicked: { + nameDialog.dialogTitle = qsTr("Новый файл") + nameDialog.actionLabel = qsTr("Создать") + nameDialog.initialText = "" + nameDialog.open() + pendingCreate.kind = "file" + pendingCreate.parent = rowMenu.targetPath + } + } + MenuItem { + text: qsTr("Создать папку внутри") + visible: rowMenu.targetIsDir + onClicked: { + nameDialog.dialogTitle = qsTr("Новая папка") + nameDialog.actionLabel = qsTr("Создать") + nameDialog.initialText = "" + nameDialog.open() + pendingCreate.kind = "folder" + pendingCreate.parent = rowMenu.targetPath + } + } MenuItem { text: qsTr("Переименовать") onClicked: { @@ -346,20 +370,24 @@ Item { property var renameTarget: ({ path: "" }) property var deleteTarget: ({ path: "" }) - property var pendingCreate: ({ kind: "file" }) + property var pendingCreate: ({ kind: "file", parent: "" }) NameDialog { id: nameDialog onConfirmed: (name) => { let err = "" + const createIn = pendingCreate.parent.length > 0 + ? pendingCreate.parent + : parentForCreate() if (pendingCreate.kind === "file") { - err = fileStore.createFile(parentForCreate(), name) + err = fileStore.createFile(createIn, name) } else if (pendingCreate.kind === "folder") { - err = fileStore.createFolder(parentForCreate(), name) + err = fileStore.createFolder(createIn, name) } else if (pendingCreate.kind === "rename") { err = fileStore.renameItem(renameTarget.path, name) } if (err.length > 0) showStatus(err, false) + pendingCreate.parent = "" } } diff --git a/qml/panels/GraphView.qml b/qml/panels/GraphView.qml index 5eafcd6..82a7d29 100644 --- a/qml/panels/GraphView.qml +++ b/qml/panels/GraphView.qml @@ -11,6 +11,9 @@ Canvas { property var pos: ({}) // key -> {x,y} property string dragKey: "" + property real zoom: 1.0 + property real panX: 0 + property real panY: 0 function nodeForKey(key) { for (let i = 0; i < nodes.length; ++i) @@ -21,13 +24,14 @@ Canvas { function relayout() { pos = {} const n = nodes.length - if (n === 0) return + if (n === 0) { requestPaint(); 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) } } + zoom = 1.0; panX = 0; panY = 0 requestPaint() } @@ -35,13 +39,18 @@ Canvas { onWidthChanged: relayout() onHeightChanged: relayout() - onPaint: { + function paintCanvas() { const ctx = getContext("2d") ctx.clearRect(0, 0, width, height) if (nodes.length === 0) return + ctx.save() + ctx.translate(width / 2 + panX, height / 2 + panY) + ctx.scale(zoom, zoom) + ctx.translate(-width / 2, -height / 2) + // рёбра - ctx.lineWidth = 1 + ctx.lineWidth = 1 / zoom ctx.strokeStyle = "#b9c6d6" for (let i = 0; i < edges.length; ++i) { const f = pos[edges[i].from] @@ -70,21 +79,34 @@ Canvas { 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) + 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) } + + ctx.restore() } + onPaint: paintCanvas() + MouseArea { anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + 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() + } onPressed: (m) => { dragKey = "" + // Переводим экранные координаты в координаты графа + const gx = (m.x - width / 2 - panX) / zoom + width / 2 + const gy = (m.y - height / 2 - panY) / zoom + height / 2 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 + const dx = gx - p.x, dy = gy - p.y + if (dx * dx + dy * dy <= 256) { // радиус захвата 16px dragKey = key break } @@ -92,16 +114,28 @@ Canvas { } onPositionChanged: (m) => { if (dragKey.length > 0 && g.pos[dragKey]) { - g.pos[dragKey] = { x: m.x, y: m.y } + const gx = (m.x - width / 2 - panX) / zoom + width / 2 + const gy = (m.y - height / 2 - panY) / zoom + height / 2 + g.pos[dragKey] = { x: gx, y: gy } + g.requestPaint() + } else if (m.buttons & Qt.RightButton) { + // Перетаскивание фона (pan) + panX += m.x - m.__lastX + panY += m.y - m.__lastY g.requestPaint() } + m.__lastX = m.x + m.__lastY = m.y } onReleased: (m) => { if (dragKey.length === 0) { + // Клик по узлу (открыть файл) + const gx = (m.x - width / 2 - panX) / zoom + width / 2 + const gy = (m.y - height / 2 - panY) / zoom + height / 2 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 dx = gx - p.x, dy = gy - p.y + if (dx * dx + dy * dy <= 144) { const nd = g.nodeForKey(key) if (nd && nd.type === "file") g.fileClicked(nd.key) @@ -111,5 +145,9 @@ Canvas { } dragKey = "" } + onDoubleClicked: (m) => { + // Двойной клик — сброс позиции/масштаба + g.zoom = 1.0; g.panX = 0; g.panY = 0; g.relayout() + } } } \ No newline at end of file diff --git a/qml/panels/MonthCalendar.qml b/qml/panels/MonthCalendar.qml index 8466b8d..aeb3c82 100644 --- a/qml/panels/MonthCalendar.qml +++ b/qml/panels/MonthCalendar.qml @@ -88,6 +88,13 @@ Item { return { day: t.getDate(), month: t.getMonth(), year: t.getFullYear() } } + // Проверяем, есть ли заметка за этот день + readonly property bool hasNote: { + if (modelData <= 0 || !fileStore.ready) return false + const ds = Qt.formatDate(new Date(cal.year, cal.month, modelData), "yyyy-MM-dd") + return fileStore.fileExists(fileStore.vault + "/Журнал/" + ds + ".md") + } + Label { anchors.centerIn: parent visible: modelData > 0 @@ -96,6 +103,18 @@ Item { color: modelData > 0 ? "#222" : "#ddd" } + // Точка — есть заметка за этот день + Rectangle { + visible: modelData > 0 && parent.hasNote + width: 5 + height: 5 + radius: 3 + color: "#3b82f6" + anchors.bottom: parent.bottom + anchors.bottomMargin: 3 + anchors.horizontalCenter: parent.horizontalCenter + } + MouseArea { anchors.fill: parent visible: modelData > 0 diff --git a/src/core/FileStore.cpp b/src/core/FileStore.cpp index e5b5191..58788da 100644 --- a/src/core/FileStore.cpp +++ b/src/core/FileStore.cpp @@ -247,12 +247,19 @@ QString FileStore::parentDirOfRow(int row) const QString FileStore::createFile(const QString &parentPath, const QString &name) { const QString dir = parentPath.isEmpty() ? m_vault : parentPath; - const QString err = validateName(name); + const QString trimmed = name.trimmed(); + if (trimmed.isEmpty()) + return QStringLiteral("Имя файла не может быть пустым."); + // Если имя без расширения — добавляем .md + QString actualName = trimmed; + if (!actualName.contains(QLatin1Char('.'))) + actualName += QStringLiteral(".md"); + const QString err = validateName(actualName); if (!err.isEmpty()) return err; - const QString path = QDir(dir).filePath(name); + const QString path = QDir(dir).filePath(actualName); if (QFileInfo::exists(path)) - return QStringLiteral("Файл с таким именем уже существует."); + return QStringLiteral("Файл «%1» уже существует в этом каталоге.").arg(actualName); QFile f(path); if (!f.open(QIODevice::WriteOnly)) return QStringLiteral("Не удалось создать файл: %1").arg(path); @@ -390,4 +397,9 @@ bool FileStore::itemIsDir(int row) const bool FileStore::itemHasChildren(int row) const { return row >= 0 && row < m_rows.size() && m_rows[row].hasChildren; +} + +bool FileStore::fileExists(const QString &path) const +{ + return QFileInfo::exists(path); } \ No newline at end of file diff --git a/src/core/FileStore.h b/src/core/FileStore.h index 0ec0b1c..b6b07ff 100644 --- a/src/core/FileStore.h +++ b/src/core/FileStore.h @@ -58,6 +58,7 @@ public: Q_INVOKABLE QString itemPath(int row) const; Q_INVOKABLE bool itemIsDir(int row) const; Q_INVOKABLE bool itemHasChildren(int row) const; + Q_INVOKABLE bool fileExists(const QString &path) const; signals: void vaultChanged();