import QtQuick import QtQuick.Controls import QtQuick.Layouts // Панель одного документа: шапка (заголовок, режим, действия) + текст. // Режимы: редактирование (TextArea) / просмотр (read-only TextArea). Item { id: pane property int docIndex: -1 property bool editMode: true property bool canOpenBeside: false // кнопка «открыть рядом» (заглавная панель) property bool closable: false // кнопка «закрыть панель» (вторая панель) signal openBeside() signal closePane() function refreshHeader() { const title = documentManager.titleAt(pane.docIndex) headerTitle.text = title + (documentManager.isDirty(pane.docIndex) ? " •" : "") } readonly property bool hasFocus: editor.activeFocus || viewer.activeFocus // ---------- Функции форматирования (используются FormatToolbar) ---------- function syncText() { documentManager.updateContent(pane.docIndex, editor.text) } function lineStart(pos) { const t = editor.text let i = Math.min(Math.max(pos, 0), t.length) while (i > 0 && t[i - 1] !== "\n") --i return i } function lineEnd(pos) { const t = editor.text let i = Math.min(Math.max(pos, 0), t.length) while (i < t.length && t[i] !== "\n") ++i return i } function replaceRange(start, end, newText) { editor.remove(start, end) editor.insert(start, newText) editor.cursorPosition = start + newText.length syncText() editor.forceActiveFocus() } // Обёртка выделения pre…post (жирный, курсив и т.п.) function applyInline(pre, post) { const s = editor.selectionStart, e = editor.selectionEnd const sel = editor.selectedText if (s === e) { const cp = editor.cursorPosition editor.insert(cp, pre + post) editor.cursorPosition = cp + pre.length } else { const t = pre + sel + post editor.remove(s, e) editor.insert(s, t) editor.cursorPosition = s + pre.length + sel.length } syncText() editor.forceActiveFocus() } // Префикс к каждой строке выделения (списки, цитата) function applyLinePrefix(prefix) { let s = editor.selectionStart, e = editor.selectionEnd if (s === e) { s = lineStart(editor.cursorPosition); e = lineEnd(editor.cursorPosition) } else { s = lineStart(s); e = lineEnd(e) } const lines = editor.text.substring(s, e).split("\n") for (let i = 0; i < lines.length; ++i) lines[i] = prefix + lines[i] replaceRange(s, e, lines.join("\n")) } // Переключение задач: "- [ ]"/"- [x]" ⇄ "- " function applyTaskToggle() { let s = editor.selectionStart, e = editor.selectionEnd if (s === e) { s = lineStart(editor.cursorPosition); e = lineEnd(editor.cursorPosition) } else { s = lineStart(s); e = lineEnd(e) } const lines = editor.text.substring(s, e).split("\n") for (let i = 0; i < lines.length; ++i) { const ln = lines[i] if (/^[-*]\s+\[[ x]\]\s+/.test(ln)) lines[i] = ln.replace(/^([-*])\s+\[[ x]\]\s+/, "$1 ") else lines[i] = "- [ ] " + ln } replaceRange(s, e, lines.join("\n")) } // Заголовок уровня 1..6 function applyHeading(level) { let s = editor.selectionStart, e = editor.selectionEnd if (s === e) { s = lineStart(editor.cursorPosition); e = lineEnd(editor.cursorPosition) } else { s = lineStart(s); e = lineEnd(e) } const lines = editor.text.substring(s, e).split("\n") for (let i = 0; i < lines.length; ++i) lines[i] = "#".repeat(level) + " " + lines[i].replace(/^#+\s*/, "") replaceRange(s, e, lines.join("\n")) } function applyColor(hex, isHighlight) { if (isHighlight) applyInline("", "") else applyInline("", "") } function applySubSup(kind) { applyInline(kind === "sup" ? "" : "", kind === "sup" ? "" : "") } function insertText(t) { editor.insert(editor.cursorPosition, t) syncText() editor.forceActiveFocus() } // Обтекание картинок/таблиц директивой ![[...|float:left|width:40%]] function applyFloatWrap() { const s = editor.selectionStart, e = editor.selectionEnd const sel = editor.selectedText.trim() if (sel.length > 0) { let inner = sel if (/^!\[\[.+\]\]$/.test(sel)) inner = sel.slice(3, -2) const text = "![[" + inner + (inner.includes("|") ? "" : "|float:left|width:40%") + "]]" editor.remove(s, e) editor.insert(s, text) editor.cursorPosition = s + text.length } else { const t = "![[image.png|float:left|width:40%]]" editor.insert(editor.cursorPosition, t) editor.cursorPosition += t.length } syncText() editor.forceActiveFocus() } // Выноска: > [!TYPE] + строки выделения function applyCallout(type) { let s = editor.selectionStart, e = editor.selectionEnd if (s === e) { s = lineStart(editor.cursorPosition); e = lineEnd(editor.cursorPosition) } else { s = lineStart(s); e = lineEnd(e) } const lines = editor.text.substring(s, e).split("\n").filter(l => l.trim().length > 0) const out = ["> [!NOTE]".replace("NOTE", type)] if (lines.length > 0) for (const l of lines) out.push("> " + l) else out.push("> Текст выноски") replaceRange(s, e, out.join("\n")) } ColumnLayout { anchors.fill: parent spacing: 0 Rectangle { Layout.fillWidth: true implicitHeight: 30 color: "whitesmoke" RowLayout { anchors.fill: parent anchors.leftMargin: 8 anchors.rightMargin: 4 spacing: 6 Label { id: headerTitle Layout.fillWidth: true elide: Text.ElideMiddle color: "#444" text: "" } // Переключатель режимов ToolButton { text: "✎" checkable: true checked: pane.editMode ToolTip.visible: hovered ToolTip.text: qsTr("Режим редактирования (Ctrl+E)") onClicked: { pane.editMode = true; editBtn.checked = true; viewBtn.checked = false } } ToolButton { id: viewBtn text: "👁" checkable: true checked: !pane.editMode ToolTip.visible: hovered ToolTip.text: qsTr("Режим просмотра (Ctrl+E)") onClicked: { pane.editMode = false; editBtn.checked = false; viewBtn.checked = true } } ToolButton { id: editBtn visible: false checkable: true checked: true onClicked: { pane.editMode = true; viewBtn.checked = false } } ToolButton { text: "\u25B8\u25CB\u25C0" // «открыть рядом» (две панели) visible: pane.canOpenBeside && !documentManager.sideBySide ToolTip.visible: hovered ToolTip.text: qsTr("Открыть рядом (две панели)") onClicked: pane.openBeside() } ToolButton { text: "\u2715" visible: pane.closable ToolTip.visible: hovered ToolTip.text: qsTr("Закрыть вторую панель") onClicked: pane.closePane() } } } Rectangle { Layout.fillWidth: true Layout.fillHeight: true color: "white" TextArea { id: editor anchors.fill: parent visible: pane.editMode text: documentManager.contentAt(pane.docIndex) onTextEdited: documentManager.updateContent(pane.docIndex, text) selectByMouse: true font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace" font.pixelSize: 14 wrapMode: TextEdit.NoWrap padding: 10 placeholderText: qsTr("Документ не выбран") } TextArea { id: viewer anchors.fill: parent visible: !pane.editMode textFormat: TextEdit.RichText text: "" readOnly: true selectByMouse: true wrapMode: TextEdit.Wrap padding: 12 onLinkActivated: (href) => pane.handleViewLink(href) placeholderText: qsTr("Документ не выбран") } } } // ---------- Просмотр (режим просмотра) ---------- function pathDir(p) { const idx = p.lastIndexOf("/") return idx >= 0 ? p.substring(0, idx) : p } function renderView() { if (pane.editMode || pane.docIndex < 0) return viewer.text = mdParser.render(documentManager.contentAt(pane.docIndex), fileStore.vault, pathDir(documentManager.pathAt(pane.docIndex))) } function handleViewLink(href) { if (href.startsWith("flora://task/")) { const line = Number(href.substring("flora://task/".length)) if (line > 0) pane.toggleTaskLine(line) return } if (href.startsWith("flora://open/")) { const path = decodeURIComponent(href.substring("flora://open/".length)) if (path.length > 0) documentManager.openDocument(path) return } if (href.startsWith("flora://wiki/")) { // Фаза 8: создание через базу связей; пока — статус. console.log("wiki:", decodeURIComponent(href)) return } console.log("внешняя ссылка:", href) } function toggleTaskLine(line) { const text = documentManager.contentAt(pane.docIndex) const lines = text.split("\n") const ln = lines[line - 1] if (ln === undefined) return if (/\[ \]/.test(ln)) lines[line - 1] = ln.replace("[ ]", "[x]") else if (/\[x\]/.test(ln)) lines[line - 1] = ln.replace("[x]", "[ ]") documentManager.updateContent(pane.docIndex, lines.join("\n")) pane.renderView() } // Свежий заголовок при смене признака «изменено» Connections { target: documentManager function onDirtyFlagChanged(i) { if (i === pane.docIndex) pane.refreshHeader() } } // Просмотр обновляется при изменении контента (например, при правке в соседней панели) Connections { target: documentManager function onContentChanged(i) { if (i === pane.docIndex && !pane.editMode) pane.renderView() } } onDocIndexChanged: { pane.refreshHeader() pane.renderView() } onEditModeChanged: { if (!pane.editMode) pane.renderView() } Component.onCompleted: pane.refreshHeader() }