Files
Flora/qml/panels/RightPanel.qml
T
grendervill 277a87a53b UX: handles 12px хит-зона по всей высоте, календарь шире, Enter починен
- SplitView handle: implicit 12 (было 1/6), Rectangle fill parent + hit 16, cursor SplitH/VCursor, для файлового дерева/живого просмотра/графа-календаря — теперь легко попасть по всей высоте
- MonthCalendar: header 28x28 radius 14, month Label fill 70-110 + year 42/38, font 11, elide, spacing 4 — 'сентябрь 2026' не налезает, кнопки 28 круглые opaque (surface/hover/selected)
- RightPanel: удалён дубль 'Сегодня' ☷ из calBox header (оставлен только ⌂ в MonthCalendar)
- EditorPanel: Enter через Keys.onReturnPressed/onEnterPressed handleEnter() (сохранение indent), убран дубль в onPressed — теперь новая строка работает
2026-09-01 22:37:40 +03:00

321 lines
14 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Flora
Item {
id: root
function activePath() { return documentManager.pathAt(documentManager.activeIndex) }
function refreshLinksTags() {
const p = root.activePath()
linksModel.clear()
if (p.length > 0) {
const links = vaultIndex.linksOfFile(p)
for (let i = 0; i < links.length; ++i) linksModel.append({ text: links[i].text, path: links[i].path })
}
tagsModel.clear()
const tags = p.length > 0 ? vaultIndex.tagsOfFile(p) : []
for (let i = 0; i < tags.length; ++i) tagsModel.append({ tag: tags[i] })
}
Component.onCompleted: { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
Connections {
target: fileStore
function onVaultChanged() { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
function onReadyChanged() { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
}
Connections {
target: documentManager
function onActiveIndexChanged() { root.refreshLinksTags() }
function onOpenTabsChanged() { root.refreshLinksTags() }
function onContentChanged(i) { if (i === documentManager.activeIndex) { root.refreshLinksTags(); rebuildDebounce.restart() } }
function onDocumentSaved() { rebuildDebounce.restart() }
}
Connections { target: vaultIndex; function onIndexChanged() { root.refreshLinksTags() } }
Timer { id: rebuildDebounce; interval: 800; onTriggered: if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
// Делаем панели ресайзабельными внутри правой колонки — увеличенная зона по всей ширине
SplitView {
anchors.fill: parent
orientation: Qt.Vertical
handle: Rectangle {
implicitHeight: 12
color: "transparent"
Rectangle {
anchors.centerIn: parent
height: 1; width: parent.width
color: Theme.divider
}
Rectangle {
anchors.fill: parent
color: "transparent"
MouseArea {
anchors.fill: parent
cursorShape: Qt.SplitVCursor
hoverEnabled: true
onPressed: mouse.accepted = false
}
}
}
// Календарь
Rectangle {
id: calBox
SplitView.preferredHeight: 240
SplitView.minimumHeight: 180
SplitView.maximumHeight: 360
color: Theme.bgAlt
border.color: Theme.divider
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
height: 28
color: "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8; anchors.rightMargin: 4
Label { text: qsTr("Календарь"); font.bold: true; color: Theme.textSecondary; font.pixelSize: 12 }
Item { Layout.fillWidth: true }
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.divider }
MonthCalendar {
id: cal
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: 4
onDaySelected: (ds) => {
const path = fileStore.dailyNotePath(ds)
if (path.length > 0) documentManager.openDocument(path)
}
}
}
}
// Ссылки и теги
Rectangle {
id: linksBox
SplitView.preferredHeight: 260
SplitView.minimumHeight: 120
color: Theme.bg
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: parent.width - 12
x: 6
y: 6
spacing: 8
RowLayout {
Layout.fillWidth: true
Label { text: qsTr("Ссылки и теги"); font.bold: true; color: Theme.textSecondary; font.pixelSize: 12 }
Item { Layout.fillWidth: true }
Label {
text: vaultIndex.fileCount + " " + qsTr("файлов")
color: Theme.textFaint
font.pixelSize: 10
}
}
Label {
text: root.activePath().length > 0 ? qsTr("Файл: %1").arg(root.baseName(activePath())) : qsTr("Нет активного файла")
elide: Text.ElideMiddle
color: Theme.textMuted
Layout.fillWidth: true
font.pixelSize: 11
}
// Ссылки
Rectangle {
Layout.fillWidth: true
color: Theme.bgAlt
radius: 8
border.color: Theme.border
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
Label { text: qsTr("Ссылки"); color: Theme.textSecondary; font.pixelSize: 11; font.bold: true }
Repeater {
model: ListModel { id: linksModel }
// placeholder, используем ListView внутри
}
ListView {
id: linksList
Layout.fillWidth: true
Layout.preferredHeight: Math.min(140, contentHeight)
model: linksModel
clip: true
delegate: ItemDelegate {
width: ListView.view.width
height: 28
text: model.text
font.pixelSize: 12
palette.text: Theme.text
background: Rectangle { color: hovered ? Theme.surfaceHover : "transparent"; radius: 6 }
onClicked: documentManager.openDocument(model.path)
}
ScrollBar.vertical: ScrollBar {}
}
Label {
visible: linksModel.count === 0
text: qsTr("— нет ссылок —")
color: Theme.textFaint
font.pixelSize: 11
font.italic: true
}
}
}
// Теги
Rectangle {
Layout.fillWidth: true
color: Theme.bgAlt
radius: 8
border.color: Theme.border
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 6
Label { text: qsTr("Теги"); color: Theme.textSecondary; font.pixelSize: 11; font.bold: true }
Flow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: tagsModel
delegate: TagChip {
text: "#" + model.tag
onClicked: {
tagFilesModel.clear()
const files = vaultIndex.filesWithTag(model.tag)
for (let i=0;i<files.length;++i) tagFilesModel.append({ path: files[i], name: baseName(files[i]) })
tagFilesTitle.text = qsTr("Файлы с тегом #%1 (%2)").arg(model.tag).arg(files.length)
tagFilesPopup.open()
}
}
}
Label {
visible: tagsModel.count === 0
text: qsTr("— нет тегов —")
color: Theme.textFaint
font.pixelSize: 11
font.italic: true
}
}
}
}
Item { Layout.fillHeight: true }
}
}
}
// Граф
Rectangle {
id: graphBox
SplitView.fillHeight: true
SplitView.minimumHeight: 160
color: Theme.graphBg
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
height: 32
color: "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8; anchors.rightMargin: 4
Label { text: qsTr("Граф связей"); font.bold: true; color: Theme.textSecondary; font.pixelSize: 12 }
Item { Layout.fillWidth: true }
Label { text: vaultIndex.nodes.length + "·" + vaultIndex.edges.length; color: Theme.textFaint; font.pixelSize: 10 }
ToolButton {
text: "\u21BB"
font.pixelSize: 12
ToolTip.text: qsTr("Обновить")
ToolTip.visible: hovered
onClicked: vaultIndex.rebuild(fileStore.vault)
}
ToolButton {
text: "\u2630"
ToolTip.text: qsTr("Справка")
ToolTip.visible: hovered
onClicked: graphHelp.open()
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.divider }
GraphView {
id: graph
Layout.fillWidth: true
Layout.fillHeight: true
nodes: vaultIndex.nodes
edges: vaultIndex.edges
onFileClicked: (path) => documentManager.openDocument(path)
}
// легенда
Row {
Layout.fillWidth: true
Layout.margins: 6
spacing: 12
Row { spacing: 4; Rectangle { width: 10; height: 10; radius: 5; color: Theme.graphNodeFile; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("Файл"); color: Theme.textMuted; font.pixelSize: 10 } }
Row { spacing: 4; Rectangle { width: 10; height: 10; radius: 5; color: Theme.graphNodeTag; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("Тег"); color: Theme.textMuted; font.pixelSize: 10 } }
Label { text: qsTr("Колесо — зум, перетаскивание — перемещение"); color: Theme.textFaint; font.pixelSize: 9; elide: Text.ElideRight; Layout.fillWidth: true; horizontalAlignment: Text.AlignRight }
}
}
}
}
function baseName(p) { const idx = p.lastIndexOf("/"); return idx >= 0 ? p.substring(idx+1) : p }
Popup {
id: tagFilesPopup
modal: true
anchors.centerIn: parent
width: 360
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
ColumnLayout {
width: parent.width - 16
x: 8; y: 8
Label { id: tagFilesTitle; font.bold: true; color: Theme.text }
ScrollView {
Layout.preferredHeight: Math.min(260, contentHeight)
Layout.fillWidth: true
clip: true
ListView {
model: ListModel { id: tagFilesModel }
delegate: ItemDelegate {
width: ListView.view.width
text: model.name
onClicked: { documentManager.openDocument(model.path); tagFilesPopup.close() }
}
}
}
Button { text: qsTr("Закрыть"); Layout.alignment: Qt.AlignRight; onClicked: tagFilesPopup.close() }
}
}
Popup {
id: graphHelp
anchors.centerIn: parent
width: 320
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
ColumnLayout {
width: parent.width - 16
x: 8; y: 8
spacing: 8
Label { text: qsTr("Граф связей"); font.bold: true; color: Theme.text }
Label { text: qsTr("• Синие узлы — файлы, зелёные — теги\n• Клик по файлу — открыть\n• Перетаскивание — переместить узел\n• Колесо — зум, перетаскивание фона — панорама\n• Двойной клик — сброс"); color: Theme.textSecondary; wrapMode: Text.Wrap; Layout.fillWidth: true; font.pixelSize: 11 }
Button { text: qsTr("Понятно"); Layout.alignment: Qt.AlignRight; onClicked: graphHelp.close() }
}
}
// скрытая модель тегов (для Flow)
ListModel { id: tagsModel }
}