import QtQuick import QtQuick.Controls import QtQuick.Layouts import Flora Item { id: pane property int docIndex: -1 property bool editMode: true property bool canOpenBeside: false property bool closable: false signal openBeside() signal closePane() function insertText(t) { textDisplay.insert(textDisplay.cursorPosition, t) } function refresh() { const idx = pane.docIndex if (idx >= 0) { const c = documentManager.contentAt(idx) if (textDisplay.text !== c && !textDisplay.activeFocus) textDisplay.text = c if (!pane.editMode) { viewDisplay.text = mdParser.render(c, fileStore.vault, filePathDir(documentManager.pathAt(idx))) } else { viewDisplay.text = "" } } else { textDisplay.text = "" viewDisplay.text = "" } } function filePathDir(p) { const i = p.lastIndexOf("/") return i >= 0 ? p.substring(0, i) : p } onDocIndexChanged: refresh() onEditModeChanged: refresh() Connections { target: documentManager function onContentChanged(idx) { if (idx === pane.docIndex && !textDisplay.activeFocus) refresh() } function onActiveIndexChanged() { if (documentManager.activeIndex === pane.docIndex) refresh() } } Component.onCompleted: refresh() function applyInline(pre, post) { if (!pane.editMode) return textDisplay.forceActiveFocus() const s = textDisplay.selectionStart, e = textDisplay.selectionEnd if (s !== e) { const sel = textDisplay.selectedText textDisplay.remove(s, e) textDisplay.insert(s, pre + sel + post) textDisplay.select(s, s + pre.length + sel.length + post.length) } else { const pos = textDisplay.cursorPosition textDisplay.insert(pos, pre + post) textDisplay.cursorPosition = pos + pre.length } documentManager.updateContent(pane.docIndex, textDisplay.text) } function applyHeading(level) { if (!pane.editMode) return textDisplay.forceActiveFocus() const txt = textDisplay.text const pos = textDisplay.cursorPosition let start = txt.lastIndexOf("\n", pos - 1) + 1 let end = txt.indexOf("\n", pos) if (end < 0) end = txt.length let line = txt.substring(start, end) line = line.replace(/^#{1,6}\s*/, "") const newLine = "#".repeat(level) + " " + line textDisplay.remove(start, end) textDisplay.insert(start, newLine) textDisplay.cursorPosition = start + newLine.length documentManager.updateContent(pane.docIndex, textDisplay.text) } function applyLinePrefix(prefix) { if (!pane.editMode) return textDisplay.forceActiveFocus() const txt = textDisplay.text const s = textDisplay.selectionStart, e = textDisplay.selectionEnd let start = txt.lastIndexOf("\n", s - 1) + 1 let finish = txt.indexOf("\n", e) if (finish < 0) finish = txt.length const block = txt.substring(start, finish) const newBlock = block.split("\n").map(l => prefix + l).join("\n") textDisplay.remove(start, finish) textDisplay.insert(start, newBlock) textDisplay.select(start, start + newBlock.length) documentManager.updateContent(pane.docIndex, textDisplay.text) } function applyTaskToggle() { applyLinePrefix("- [ ] ") } function applyCallout(kind) { applyLinePrefix("> [!" + kind + "] ") } function applyFloatWrap() { if (!pane.editMode) return textDisplay.forceActiveFocus() textDisplay.insert(textDisplay.cursorPosition, "![[image.png|float:left|width:40%]]") documentManager.updateContent(pane.docIndex, textDisplay.text) } function applyColor(color, highlightMode) { if (highlightMode) applyInline('', '') else applyInline('', '') } function applySubSup(kind) { if (kind === "sup") applyInline("", "") else applyInline("", "") } // --- Текстовый редактор --- ScrollView { id: editScroll anchors.fill: parent anchors.bottomMargin: 36 visible: pane.editMode clip: true ScrollBar.vertical.policy: ScrollBar.AsNeeded TextArea { id: textDisplay placeholderText: qsTr("Начните писать…") placeholderTextColor: Theme.textFaint color: Theme.editorText selectionColor: Theme.selectionBg selectedTextColor: Theme.text font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace" font.pixelSize: 14 wrapMode: TextArea.NoWrap selectByMouse: true background: Rectangle { color: Theme.editorBg } onTextChanged: if (activeFocus && text !== documentManager.contentAt(pane.docIndex)) editDebounce.restart() } } Timer { id: editDebounce; interval: 250; onTriggered: documentManager.updateContent(pane.docIndex, textDisplay.text) } ScrollView { id: viewScroll anchors.fill: parent anchors.bottomMargin: 36 visible: !pane.editMode clip: true ScrollBar.vertical.policy: ScrollBar.AsNeeded TextArea { id: viewDisplay readOnly: true textFormat: Text.RichText color: Theme.text selectionColor: Theme.selectionBg font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace" font.pixelSize: 14 wrapMode: TextArea.Wrap background: Rectangle { color: Theme.editorBg } onLinkActivated: (href) => { if (href.startsWith("flora://copycode/")) { const id = href.substring(17) const code = mdParser.getCodeBlock(id) if (code.length > 0) { mdParser.copyToClipboard(code); copyFeedback.visible = true; copyTimer.restart() } return } if (href.startsWith("flora://task/")) { const line = Number(href.substring(14)) if (line > 0) { const lines = textDisplay.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]", "[ ]") textDisplay.text = lines.join("\n") documentManager.updateContent(pane.docIndex, textDisplay.text) } } } else if (href.startsWith("flora://open/")) { documentManager.openDocument(decodeURIComponent(href.substring(13))) } else { Qt.openUrlExternally(href) } } } // Тост копирования Rectangle { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: 8 visible: copyFeedback.visible color: "#333" radius: 6 width: copyFeedback.implicitWidth + 16 height: copyFeedback.implicitHeight + 10 z: 11 Label { id: copyFeedback; anchors.centerIn: parent; visible: false; text: qsTr("Скопировано!"); color: "white"; font.pixelSize: 11 } } Timer { id: copyTimer; interval: 1500; onTriggered: copyFeedback.visible = false } } // --- Нижняя панель --- Rectangle { id: toolbar anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right height: 36 z: 10 color: Theme.toolbarBg border.color: Theme.divider RowLayout { anchors.fill: parent anchors.leftMargin: 8 anchors.rightMargin: 8 spacing: 4 Label { visible: pane.docIndex >= 0 text: pane.docIndex >= 0 ? documentManager.titleAt(pane.docIndex) : "" color: Theme.textMuted font.pixelSize: 11 elide: Text.ElideMiddle Layout.fillWidth: true } ToolButton { text: "✎" checkable: true checked: pane.editMode ToolTip.text: qsTr("Редактирование") ToolTip.visible: hovered onClicked: pane.editMode = true } ToolButton { text: "👁" checkable: true checked: !pane.editMode ToolTip.text: qsTr("Просмотр") ToolTip.visible: hovered onClicked: pane.editMode = false } ToolButton { text: "\u2715" visible: pane.closable ToolTip.text: qsTr("Закрыть") ToolTip.visible: hovered onClicked: pane.closePane() } } } }