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()
+}