From 6bf8e3ea1576da67af91700c4df0b38ce87a4729 Mon Sep 17 00:00:00 2001 From: "Flora (opencode)" Date: Mon, 17 Aug 2026 19:22:41 +0300 Subject: [PATCH] =?UTF-8?q?Fix:=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=D1=8E=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=20EditorPane=20=E2=80=94=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=8F=20TextArea=20anchors.fill:=20parent=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20ColumnLayout,=20=D0=B1=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BC=D0=B5=D0=B6=D1=83=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=B2;=20=D1=88=D0=B0=D0=BF=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=BA=20overlay=20z:10;=20viewer=20=D1=81=20handle?= =?UTF-8?q?ViewLink;=20refresh=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0/=D1=80=D0=B5?= =?UTF-8?q?=D0=B6=D0=B8=D0=BC=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qml/panels/EditorPane.qml | 341 ++++++++------------------------------ 1 file changed, 68 insertions(+), 273 deletions(-) diff --git a/qml/panels/EditorPane.qml b/qml/panels/EditorPane.qml index 61c6297..69e374e 100644 --- a/qml/panels/EditorPane.qml +++ b/qml/panels/EditorPane.qml @@ -2,185 +2,23 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -// qml-disable-aot - -// Панель одного документа: шапка (заголовок, режим, действия) + текст. -// Режимы: редактирование (TextArea) / просмотр (read-only TextArea). Item { id: pane property int docIndex: -1 property bool editMode: true - property bool canOpenBeside: false // кнопка «открыть рядом» (заглавная панель) - property bool closable: false // кнопка «закрыть панель» (вторая панель) - property string editorContent: "" // явное содержимое для TextArea (не биндинг на функцию) + 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")) } - // ---------- TextArea (заполняет ВСЁ пространство) ---------- - TextArea { - id: editor - anchors.fill: parent - visible: pane.editMode - color: "black" - 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("Документ не выбран") - } - - // ---------- Шапка файла (плавающий оверлей сверху) ---------- + // Шапка файла — плавающий оверлей Rectangle { - id: headerBar anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -196,144 +34,101 @@ Item { spacing: 6 Label { - id: headerTitle Layout.fillWidth: true elide: Text.ElideMiddle color: "#444" - text: "" + text: documentManager.titleAt(pane.docIndex) } ToolButton { text: "✎" checkable: true checked: pane.editMode - ToolTip.text: qsTr("Редактирование") - onClicked: { pane.editMode = true; editBtn.checked = true; viewBtn.checked = false } + onClicked: pane.editMode = true } ToolButton { - id: viewBtn text: "👁" checkable: true checked: !pane.editMode - ToolTip.text: qsTr("Просмотр") - 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.text: qsTr("Открыть рядом") - onClicked: pane.openBeside() + onClicked: pane.editMode = false } ToolButton { text: "\u2715" visible: pane.closable - ToolTip.text: qsTr("Закрыть панель") onClicked: pane.closePane() } } } - // ---------- Функции ---------- - function loadContent() { - if (pane.docIndex >= 0) { - const c = documentManager.contentAt(pane.docIndex) - // Принудительное обновление TextArea: сначала очищаем, затем ставим контент - editor.text = "" - editor.text = c - editor.cursorPosition = c.length - pane.editorContent = c - } else { - editor.text = "" - pane.editorContent = "" - } - } - 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() + // TextArea — заполняет ВСЁ пространство + TextArea { + id: editor + anchors.fill: parent + visible: pane.editMode + color: "black" + font.family: "monospace" + font.pixelSize: 14 + padding: 10 + onTextEdited: documentManager.updateContent(pane.docIndex, text) } - // Свежий заголовок при смене признака «изменено» - Connections { - target: documentManager - function onDirtyFlagChanged(i) { - if (i === pane.docIndex) - pane.refreshHeader() - } - } - // Просмотр обновляется при изменении контента - Connections { - target: documentManager - function onActiveIndexChanged() { - pane.loadContent() - } - function onContentChanged(i) { - if (i === pane.docIndex) { - pane.loadContent() - if (!pane.editMode) - pane.renderView() + // Просмотр + TextArea { + id: viewer + anchors.fill: parent + visible: !pane.editMode + readOnly: true + color: "black" + font.family: "monospace" + font.pixelSize: 14 + padding: 10 + onLinkActivated: (href) => { + if (href.startsWith("flora://task/")) { + const line = Number(href.substring(14)) + if (line > 0) { + const lines = editor.text.split("\n") + const ln = lines[line - 1] + if (ln !== undefined) { + if (/\[ \]/.test(ln)) lines[line - 1] = ln.replace("[ ]", "[x]") + else if (/\[x\]/.test(ln)) lines[line - 1] = ln.replace("[x]", "[ ]") + editor.text = lines.join("\n") + documentManager.updateContent(pane.docIndex, editor.text) + } + } + } else if (href.startsWith("flora://open/")) { + documentManager.openDocument(decodeURIComponent(href.substring(13))) } } } - onDocIndexChanged: { - pane.refreshHeader() - pane.renderView() - pane.loadContent() - loadTimer.start() + // Загрузка контента при смене файла + function refresh() { + const idx = pane.docIndex + if (idx >= 0) { + const c = documentManager.contentAt(idx) + editor.text = c + viewer.text = mdParser.render(c, fileStore.vault, filePathDir(documentManager.pathAt(idx))) + } else { + editor.text = "" + viewer.text = "" + } } - Timer { - id: loadTimer - interval: 50 - onTriggered: pane.loadContent() + function filePathDir(p) { + const i = p.lastIndexOf("/") + return i >= 0 ? p.substring(0, i) : p } - onEditModeChanged: { - if (!pane.editMode) - pane.renderView() + + onDocIndexChanged: refresh() + onEditModeChanged: { if (!editMode) refresh() } + + Connections { + target: documentManager + function onActiveIndexChanged() { + if (documentManager.activeIndex === pane.docIndex) + refresh() + } } - Component.onCompleted: pane.refreshHeader() -} \ No newline at end of file + + Component.onCompleted: refresh() +}