Массовые исправления UX: вкладки (MouseArea не блокирует ×), контент редактора (onDocIndexChanged), авто-расширение .md, ошибка дубликата, ПКМ-создание внутри папок, точки в календаре, граф (зум колесом, клик по файлу, pan правой кнопкой), fileExists(), 16/16 тестов
This commit is contained in:
@@ -312,6 +312,10 @@ Item {
|
|||||||
onDocIndexChanged: {
|
onDocIndexChanged: {
|
||||||
pane.refreshHeader()
|
pane.refreshHeader()
|
||||||
pane.renderView()
|
pane.renderView()
|
||||||
|
if (pane.docIndex >= 0 && pane.editMode) {
|
||||||
|
editor.text = documentManager.contentAt(pane.docIndex)
|
||||||
|
editor.forceActiveFocus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onEditModeChanged: {
|
onEditModeChanged: {
|
||||||
if (!pane.editMode)
|
if (!pane.editMode)
|
||||||
|
|||||||
+14
-14
@@ -47,14 +47,20 @@ Item {
|
|||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
spacing: 4
|
spacing: 4
|
||||||
|
|
||||||
Label {
|
MouseArea {
|
||||||
id: tabLabel
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
elide: Text.ElideMiddle
|
Layout.fillHeight: true
|
||||||
text: root.baseName(modelData)
|
onClicked: documentManager.setActiveIndex(index)
|
||||||
+ (documentManager.isDirty(index) ? " •" : "")
|
onDoubleClicked: documentManager.closeTab(index)
|
||||||
font.pixelSize: 12
|
|
||||||
color: active ? "#111" : "#555"
|
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 {
|
ToolButton {
|
||||||
@@ -63,15 +69,9 @@ Item {
|
|||||||
implicitHeight: 20
|
implicitHeight: 20
|
||||||
font.pixelSize: 9
|
font.pixelSize: 9
|
||||||
opacity: tab.active || hovered ? 1 : 0
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
MenuItem {
|
||||||
text: qsTr("Переименовать")
|
text: qsTr("Переименовать")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -346,20 +370,24 @@ Item {
|
|||||||
|
|
||||||
property var renameTarget: ({ path: "" })
|
property var renameTarget: ({ path: "" })
|
||||||
property var deleteTarget: ({ path: "" })
|
property var deleteTarget: ({ path: "" })
|
||||||
property var pendingCreate: ({ kind: "file" })
|
property var pendingCreate: ({ kind: "file", parent: "" })
|
||||||
|
|
||||||
NameDialog {
|
NameDialog {
|
||||||
id: nameDialog
|
id: nameDialog
|
||||||
onConfirmed: (name) => {
|
onConfirmed: (name) => {
|
||||||
let err = ""
|
let err = ""
|
||||||
|
const createIn = pendingCreate.parent.length > 0
|
||||||
|
? pendingCreate.parent
|
||||||
|
: parentForCreate()
|
||||||
if (pendingCreate.kind === "file") {
|
if (pendingCreate.kind === "file") {
|
||||||
err = fileStore.createFile(parentForCreate(), name)
|
err = fileStore.createFile(createIn, name)
|
||||||
} else if (pendingCreate.kind === "folder") {
|
} else if (pendingCreate.kind === "folder") {
|
||||||
err = fileStore.createFolder(parentForCreate(), name)
|
err = fileStore.createFolder(createIn, name)
|
||||||
} else if (pendingCreate.kind === "rename") {
|
} else if (pendingCreate.kind === "rename") {
|
||||||
err = fileStore.renameItem(renameTarget.path, name)
|
err = fileStore.renameItem(renameTarget.path, name)
|
||||||
}
|
}
|
||||||
if (err.length > 0) showStatus(err, false)
|
if (err.length > 0) showStatus(err, false)
|
||||||
|
pendingCreate.parent = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-12
@@ -11,6 +11,9 @@ Canvas {
|
|||||||
|
|
||||||
property var pos: ({}) // key -> {x,y}
|
property var pos: ({}) // key -> {x,y}
|
||||||
property string dragKey: ""
|
property string dragKey: ""
|
||||||
|
property real zoom: 1.0
|
||||||
|
property real panX: 0
|
||||||
|
property real panY: 0
|
||||||
|
|
||||||
function nodeForKey(key) {
|
function nodeForKey(key) {
|
||||||
for (let i = 0; i < nodes.length; ++i)
|
for (let i = 0; i < nodes.length; ++i)
|
||||||
@@ -21,13 +24,14 @@ Canvas {
|
|||||||
function relayout() {
|
function relayout() {
|
||||||
pos = {}
|
pos = {}
|
||||||
const n = nodes.length
|
const n = nodes.length
|
||||||
if (n === 0) return
|
if (n === 0) { requestPaint(); return }
|
||||||
const cx = width / 2, cy = height / 2
|
const cx = width / 2, cy = height / 2
|
||||||
const rad = Math.min(width, height) / 2 - 30
|
const rad = Math.min(width, height) / 2 - 30
|
||||||
for (let i = 0; i < n; ++i) {
|
for (let i = 0; i < n; ++i) {
|
||||||
const a = (i / n) * 2 * Math.PI - Math.PI / 2
|
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) }
|
pos[nodes[i].key] = { x: cx + rad * Math.cos(a), y: cy + rad * Math.sin(a) }
|
||||||
}
|
}
|
||||||
|
zoom = 1.0; panX = 0; panY = 0
|
||||||
requestPaint()
|
requestPaint()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,13 +39,18 @@ Canvas {
|
|||||||
onWidthChanged: relayout()
|
onWidthChanged: relayout()
|
||||||
onHeightChanged: relayout()
|
onHeightChanged: relayout()
|
||||||
|
|
||||||
onPaint: {
|
function paintCanvas() {
|
||||||
const ctx = getContext("2d")
|
const ctx = getContext("2d")
|
||||||
ctx.clearRect(0, 0, width, height)
|
ctx.clearRect(0, 0, width, height)
|
||||||
if (nodes.length === 0) return
|
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"
|
ctx.strokeStyle = "#b9c6d6"
|
||||||
for (let i = 0; i < edges.length; ++i) {
|
for (let i = 0; i < edges.length; ++i) {
|
||||||
const f = pos[edges[i].from]
|
const f = pos[edges[i].from]
|
||||||
@@ -70,21 +79,34 @@ Canvas {
|
|||||||
ctx.lineWidth = 1
|
ctx.lineWidth = 1
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
ctx.fillStyle = "#5a6a7d"
|
ctx.fillStyle = "#5a6a7d"
|
||||||
ctx.font = "9px sans-serif"
|
ctx.font = (9 / zoom) + "px sans-serif"
|
||||||
const label = isFile ? nd.name : nd.name
|
const txt = nd.name.length > 14 ? nd.name.substring(0, 13) + "…" : nd.name
|
||||||
const txt = label.length > 14 ? label.substring(0, 13) + "…" : label
|
ctx.fillText(txt, p.x, p.y + r + 10 / zoom)
|
||||||
ctx.fillText(txt, p.x, p.y + r + 10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPaint: paintCanvas()
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
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) => {
|
onPressed: (m) => {
|
||||||
dragKey = ""
|
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) {
|
for (const key in g.pos) {
|
||||||
const p = g.pos[key]
|
const p = g.pos[key]
|
||||||
const dx = m.x - p.x, dy = m.y - p.y
|
const dx = gx - p.x, dy = gy - p.y
|
||||||
if (dx * dx + dy * dy <= 144) { // радиус захвата 12px
|
if (dx * dx + dy * dy <= 256) { // радиус захвата 16px
|
||||||
dragKey = key
|
dragKey = key
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -92,16 +114,28 @@ Canvas {
|
|||||||
}
|
}
|
||||||
onPositionChanged: (m) => {
|
onPositionChanged: (m) => {
|
||||||
if (dragKey.length > 0 && g.pos[dragKey]) {
|
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()
|
g.requestPaint()
|
||||||
}
|
}
|
||||||
|
m.__lastX = m.x
|
||||||
|
m.__lastY = m.y
|
||||||
}
|
}
|
||||||
onReleased: (m) => {
|
onReleased: (m) => {
|
||||||
if (dragKey.length === 0) {
|
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) {
|
for (const key in g.pos) {
|
||||||
const p = g.pos[key]
|
const p = g.pos[key]
|
||||||
const dx = m.x - p.x, dy = m.y - p.y
|
const dx = gx - p.x, dy = gy - p.y
|
||||||
if (dx * dx + dy * dy <= 100) {
|
if (dx * dx + dy * dy <= 144) {
|
||||||
const nd = g.nodeForKey(key)
|
const nd = g.nodeForKey(key)
|
||||||
if (nd && nd.type === "file")
|
if (nd && nd.type === "file")
|
||||||
g.fileClicked(nd.key)
|
g.fileClicked(nd.key)
|
||||||
@@ -111,5 +145,9 @@ Canvas {
|
|||||||
}
|
}
|
||||||
dragKey = ""
|
dragKey = ""
|
||||||
}
|
}
|
||||||
|
onDoubleClicked: (m) => {
|
||||||
|
// Двойной клик — сброс позиции/масштаба
|
||||||
|
g.zoom = 1.0; g.panX = 0; g.panY = 0; g.relayout()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,6 +88,13 @@ Item {
|
|||||||
return { day: t.getDate(), month: t.getMonth(), year: t.getFullYear() }
|
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 {
|
Label {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: modelData > 0
|
visible: modelData > 0
|
||||||
@@ -96,6 +103,18 @@ Item {
|
|||||||
color: modelData > 0 ? "#222" : "#ddd"
|
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 {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: modelData > 0
|
visible: modelData > 0
|
||||||
|
|||||||
+15
-3
@@ -247,12 +247,19 @@ QString FileStore::parentDirOfRow(int row) const
|
|||||||
QString FileStore::createFile(const QString &parentPath, const QString &name)
|
QString FileStore::createFile(const QString &parentPath, const QString &name)
|
||||||
{
|
{
|
||||||
const QString dir = parentPath.isEmpty() ? m_vault : parentPath;
|
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())
|
if (!err.isEmpty())
|
||||||
return err;
|
return err;
|
||||||
const QString path = QDir(dir).filePath(name);
|
const QString path = QDir(dir).filePath(actualName);
|
||||||
if (QFileInfo::exists(path))
|
if (QFileInfo::exists(path))
|
||||||
return QStringLiteral("Файл с таким именем уже существует.");
|
return QStringLiteral("Файл «%1» уже существует в этом каталоге.").arg(actualName);
|
||||||
QFile f(path);
|
QFile f(path);
|
||||||
if (!f.open(QIODevice::WriteOnly))
|
if (!f.open(QIODevice::WriteOnly))
|
||||||
return QStringLiteral("Не удалось создать файл: %1").arg(path);
|
return QStringLiteral("Не удалось создать файл: %1").arg(path);
|
||||||
@@ -391,3 +398,8 @@ bool FileStore::itemHasChildren(int row) const
|
|||||||
{
|
{
|
||||||
return row >= 0 && row < m_rows.size() && m_rows[row].hasChildren;
|
return row >= 0 && row < m_rows.size() && m_rows[row].hasChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FileStore::fileExists(const QString &path) const
|
||||||
|
{
|
||||||
|
return QFileInfo::exists(path);
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ public:
|
|||||||
Q_INVOKABLE QString itemPath(int row) const;
|
Q_INVOKABLE QString itemPath(int row) const;
|
||||||
Q_INVOKABLE bool itemIsDir(int row) const;
|
Q_INVOKABLE bool itemIsDir(int row) const;
|
||||||
Q_INVOKABLE bool itemHasChildren(int row) const;
|
Q_INVOKABLE bool itemHasChildren(int row) const;
|
||||||
|
Q_INVOKABLE bool fileExists(const QString &path) const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void vaultChanged();
|
void vaultChanged();
|
||||||
|
|||||||
Reference in New Issue
Block a user