Фаза 4: редактор — DocumentManager (вкладки, автосохранение 5с, сохранение при закрытии), EditorPane/EditorPanel (вкладки, режимы, «открыть рядом», Ctrl+S/W/E/Tab), открытие из дерева; тест docmgr (14/14)
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
#include "core/DocumentManager.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSaveFile>
|
||||
#include <QTextStream>
|
||||
|
||||
DocumentManager::DocumentManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
QTimer *autosave = new QTimer(this);
|
||||
autosave->setInterval(m_autosaveMs);
|
||||
autosave->setSingleShot(false);
|
||||
connect(autosave, &QTimer::timeout, this, &DocumentManager::saveAll);
|
||||
autosave->start();
|
||||
|
||||
// Защита при выходе
|
||||
connect(qApp, &QCoreApplication::aboutToQuit, this, &DocumentManager::saveAll);
|
||||
}
|
||||
|
||||
QStringList DocumentManager::openTabs() const
|
||||
{
|
||||
QStringList list;
|
||||
list.reserve(m_docs.size());
|
||||
for (const Doc &d : m_docs)
|
||||
list.append(d.path);
|
||||
return list;
|
||||
}
|
||||
|
||||
int DocumentManager::activeIndex() const { return m_active; }
|
||||
void DocumentManager::setActiveIndex(int index)
|
||||
{
|
||||
if (m_active == index)
|
||||
return;
|
||||
m_active = index;
|
||||
emit activeIndexChanged();
|
||||
}
|
||||
|
||||
int DocumentManager::secondaryIndex() const { return m_secondary; }
|
||||
void DocumentManager::setSecondaryIndex(int index)
|
||||
{
|
||||
if (m_secondary == index)
|
||||
return;
|
||||
m_secondary = index;
|
||||
emit secondaryIndexChanged();
|
||||
}
|
||||
|
||||
bool DocumentManager::sideBySide() const { return m_sideBySide; }
|
||||
void DocumentManager::setSideBySide(bool on)
|
||||
{
|
||||
if (m_sideBySide == on)
|
||||
return;
|
||||
m_sideBySide = on;
|
||||
emit sideBySideChanged();
|
||||
}
|
||||
|
||||
int DocumentManager::autosaveIntervalMs() const { return m_autosaveMs; }
|
||||
void DocumentManager::setAutosaveIntervalMs(int ms)
|
||||
{
|
||||
if (m_autosaveMs == ms)
|
||||
return;
|
||||
m_autosaveMs = ms;
|
||||
emit autosaveIntervalChanged();
|
||||
}
|
||||
|
||||
QString DocumentManager::lastStatus() const { return m_lastStatus; }
|
||||
|
||||
int DocumentManager::openDocument(const QString &path)
|
||||
{
|
||||
const QString clean = QFileInfo(path).absoluteFilePath();
|
||||
for (int i = 0; i < m_docs.size(); ++i) {
|
||||
if (m_docs[i].path == clean) {
|
||||
setActiveIndex(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QString content = readFile(clean, &ok);
|
||||
if (!ok) {
|
||||
m_lastStatus = QStringLiteral("Не удалось открыть файл: %1").arg(clean);
|
||||
emit lastStatusChanged();
|
||||
return -1;
|
||||
}
|
||||
|
||||
Doc doc;
|
||||
doc.path = clean;
|
||||
doc.content = content;
|
||||
doc.dirty = false;
|
||||
m_docs.append(doc);
|
||||
|
||||
const int idx = m_docs.size() - 1;
|
||||
setActiveIndex(idx);
|
||||
emit openTabsChanged();
|
||||
return idx;
|
||||
}
|
||||
|
||||
QString DocumentManager::readFile(const QString &path, bool *ok) const
|
||||
{
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
if (ok) *ok = false;
|
||||
return QString();
|
||||
}
|
||||
const QByteArray bytes = f.readAll();
|
||||
if (ok) *ok = true;
|
||||
return QString::fromUtf8(bytes);
|
||||
}
|
||||
|
||||
QString DocumentManager::contentAt(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_docs.size())
|
||||
return QString();
|
||||
return m_docs[index].content;
|
||||
}
|
||||
|
||||
QString DocumentManager::titleAt(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_docs.size())
|
||||
return QString();
|
||||
return QFileInfo(m_docs[index].path).fileName();
|
||||
}
|
||||
|
||||
QString DocumentManager::pathAt(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_docs.size())
|
||||
return QString();
|
||||
return m_docs[index].path;
|
||||
}
|
||||
|
||||
bool DocumentManager::isDirty(int index) const
|
||||
{
|
||||
return index >= 0 && index < m_docs.size() && m_docs[index].dirty;
|
||||
}
|
||||
|
||||
void DocumentManager::updateContent(int index, const QString &text)
|
||||
{
|
||||
if (index < 0 || index >= m_docs.size())
|
||||
return;
|
||||
Doc &d = m_docs[index];
|
||||
if (d.content == text)
|
||||
return;
|
||||
d.content = text;
|
||||
d.dirty = true;
|
||||
emit dirtyFlagChanged(index);
|
||||
emit contentChanged(index);
|
||||
}
|
||||
|
||||
bool DocumentManager::isTextForEditor(const QString &path) const
|
||||
{
|
||||
const QString ext = QFileInfo(path).suffix().toLower();
|
||||
static const QStringList textExts = {
|
||||
"md", "markdown", "txt", "log", "json", "yaml", "yml",
|
||||
"c", "h", "cpp", "hpp", "cc", "cxx", "py", "go", "rs",
|
||||
"js", "ts", "css", "sh", "bash", "xml", "ini", "csv", "svg"
|
||||
};
|
||||
return ext.isEmpty() || textExts.contains(ext);
|
||||
}
|
||||
|
||||
void DocumentManager::saveDocument(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_docs.size())
|
||||
return;
|
||||
Doc &d = m_docs[index];
|
||||
if (!d.dirty)
|
||||
return;
|
||||
|
||||
QSaveFile f(d.path);
|
||||
if (f.open(QIODevice::WriteOnly)) {
|
||||
f.write(d.content.toUtf8());
|
||||
if (f.commit()) {
|
||||
d.dirty = false;
|
||||
emit dirtyFlagChanged(index);
|
||||
emit documentSaved(index, d.path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_lastStatus = QStringLiteral("Не удалось сохранить: %1").arg(d.path);
|
||||
emit lastStatusChanged();
|
||||
}
|
||||
|
||||
void DocumentManager::saveAll()
|
||||
{
|
||||
for (int i = 0; i < m_docs.size(); ++i)
|
||||
saveDocument(i);
|
||||
}
|
||||
|
||||
bool DocumentManager::closeTab(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_docs.size())
|
||||
return false;
|
||||
// При закрытии тоже сохраняем (защита от потери текста).
|
||||
saveDocument(index);
|
||||
|
||||
m_docs.removeAt(index);
|
||||
|
||||
if (m_active > index)
|
||||
--m_active;
|
||||
else if (m_active == index)
|
||||
m_active = m_docs.isEmpty() ? -1 : qMin(index, m_docs.size() - 1);
|
||||
|
||||
if (m_secondary > index)
|
||||
--m_secondary;
|
||||
else if (m_secondary == index)
|
||||
m_secondary = -1;
|
||||
|
||||
emit openTabsChanged();
|
||||
emit activeIndexChanged();
|
||||
emit secondaryIndexChanged();
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
#include <QList>
|
||||
|
||||
// Управление открытыми документами: вкладки, содержимое, признак «изменён»,
|
||||
// автосохранение каждые 5 секунд (защита от потери текста).
|
||||
// Режимы редактирование/просмотр — на стороне QML (на документ).
|
||||
class DocumentManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QStringList openTabs READ openTabs NOTIFY openTabsChanged)
|
||||
Q_PROPERTY(int activeIndex READ activeIndex WRITE setActiveIndex NOTIFY activeIndexChanged)
|
||||
Q_PROPERTY(int secondaryIndex READ secondaryIndex WRITE setSecondaryIndex NOTIFY secondaryIndexChanged)
|
||||
Q_PROPERTY(bool sideBySide READ sideBySide WRITE setSideBySide NOTIFY sideBySideChanged)
|
||||
Q_PROPERTY(int autosaveIntervalMs READ autosaveIntervalMs WRITE setAutosaveIntervalMs
|
||||
NOTIFY autosaveIntervalChanged)
|
||||
Q_PROPERTY(QString lastStatus READ lastStatus NOTIFY lastStatusChanged)
|
||||
|
||||
public:
|
||||
explicit DocumentManager(QObject *parent = nullptr);
|
||||
|
||||
QStringList openTabs() const;
|
||||
int activeIndex() const;
|
||||
void setActiveIndex(int index);
|
||||
int secondaryIndex() const;
|
||||
void setSecondaryIndex(int index);
|
||||
bool sideBySide() const;
|
||||
void setSideBySide(bool on);
|
||||
int autosaveIntervalMs() const;
|
||||
void setAutosaveIntervalMs(int ms);
|
||||
QString lastStatus() const;
|
||||
|
||||
// Открывает документ (при необходимости создаётся вкладка). Возвращает индекс.
|
||||
Q_INVOKABLE int openDocument(const QString &path);
|
||||
|
||||
// Содержимое/заголовок/признак по индексу вкладки.
|
||||
Q_INVOKABLE QString contentAt(int index) const;
|
||||
Q_INVOKABLE QString titleAt(int index) const;
|
||||
Q_INVOKABLE QString pathAt(int index) const;
|
||||
Q_INVOKABLE bool isDirty(int index) const;
|
||||
|
||||
// Редактирование: пользователь изменил текст вкладки.
|
||||
Q_INVOKABLE void updateContent(int index, const QString &text);
|
||||
|
||||
// Можно ли открыть файл в текстовом редакторе (md и прочие текстовые).
|
||||
Q_INVOKABLE bool isTextForEditor(const QString &path) const;
|
||||
|
||||
Q_INVOKABLE void saveDocument(int index);
|
||||
Q_INVOKABLE void saveAll();
|
||||
|
||||
Q_INVOKABLE bool closeTab(int index);
|
||||
|
||||
signals:
|
||||
void openTabsChanged();
|
||||
void activeIndexChanged();
|
||||
void secondaryIndexChanged();
|
||||
void sideBySideChanged();
|
||||
void autosaveIntervalChanged();
|
||||
void lastStatusChanged();
|
||||
|
||||
// Уведомляет вкладку о смене признака «изменено».
|
||||
void dirtyFlagChanged(int index);
|
||||
// Содержимое документа изменилось (вкладка index).
|
||||
void contentChanged(int index);
|
||||
// Файл фактически записан на диск.
|
||||
void documentSaved(int index, const QString &path);
|
||||
|
||||
private:
|
||||
struct Doc {
|
||||
QString path;
|
||||
QString content;
|
||||
bool dirty = false;
|
||||
};
|
||||
|
||||
void ensureAutosave();
|
||||
QString readFile(const QString &path, bool *ok) const;
|
||||
|
||||
QList<Doc> m_docs;
|
||||
int m_active = -1;
|
||||
int m_secondary = -1;
|
||||
bool m_sideBySide = false;
|
||||
int m_autosaveMs = 5000;
|
||||
QString m_lastStatus;
|
||||
};
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "app/VaultManager.h"
|
||||
#include "app/WindowController.h"
|
||||
#include "core/DocumentManager.h"
|
||||
#include "core/SettingsStore.h"
|
||||
#include "git/GitManager.h"
|
||||
#include "sync/SyncManager.h"
|
||||
@@ -37,12 +38,14 @@ int main(int argc, char *argv[])
|
||||
GitManager gitManager;
|
||||
SyncManager syncManager;
|
||||
VaultManager vaultManager(&settingsStore, &gitManager);
|
||||
DocumentManager documentManager;
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
engine.rootContext()->setContextProperty("settingsStore", &settingsStore);
|
||||
engine.rootContext()->setContextProperty("gitManager", &gitManager);
|
||||
engine.rootContext()->setContextProperty("syncManager", &syncManager);
|
||||
engine.rootContext()->setContextProperty("vaultManager", &vaultManager);
|
||||
engine.rootContext()->setContextProperty("documentManager", &documentManager);
|
||||
|
||||
WindowController windowController(
|
||||
&engine, QStringLiteral("qrc:/qt/qml/Flora/qml/MainWindow.qml"));
|
||||
|
||||
Reference in New Issue
Block a user