From b8eabe41fa952b04e921ef9d6b06c7ab672d888c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 30 Jan 2022 19:03:25 +0100 Subject: [PATCH] Add a rich text file format (#26) * Add basic document writer with a somewhat verbose file format * Use a more compact text or fragment format * Indent JSON with tabs * Add basic text document parsing * Remove private class for settings * Update text formatting and add text format to settings * Add text indent button and detect text format status and apply to toolbar * Fix a couple of issues with updating block format * Add proper support for block indenting * Clean up the code a little * Add a document class and cache documents in project class * Connect story tree to document editor --- CMakeLists.txt | 1 + i18n/collett_en_US.ts | 91 +++--- i18n/collett_nb_NO.ts | 91 +++--- .../7e5a1a98-d1a3-44a1-ab4e-2b5d21d92201.json | 72 +++++ .../e709ba3f-3141-4b4b-95df-4a8d3e91a8ba.json | 10 + sample/project/project.json | 19 +- sample/project/story.json | 144 ++++----- src/collett.h | 10 +- src/core/icons.cpp | 6 + src/core/storage.cpp | 57 +++- src/core/storage.h | 3 + src/editor/doceditor.cpp | 99 +++++- src/editor/doceditor.h | 28 ++ src/editor/edittoolbar.cpp | 23 +- src/editor/edittoolbar.h | 5 + src/editor/textedit.cpp | 296 +++++++++++++++++- src/editor/textedit.h | 20 ++ src/gui/storytree.cpp | 7 +- src/guimain.cpp | 68 +++- src/guimain.h | 20 +- src/project/document.cpp | 144 +++++++++ src/project/document.h | 74 +++++ src/project/project.cpp | 68 +++- src/project/project.h | 25 +- src/project/storyitem.cpp | 4 + src/project/storyitem.h | 1 + src/project/storymodel.cpp | 9 + src/project/storymodel.h | 1 + src/settings.cpp | 189 ++++++++--- src/settings.h | 53 +++- 30 files changed, 1384 insertions(+), 254 deletions(-) create mode 100644 sample/content/7e5a1a98-d1a3-44a1-ab4e-2b5d21d92201.json create mode 100644 sample/content/e709ba3f-3141-4b4b-95df-4a8d3e91a8ba.json create mode 100644 src/project/document.cpp create mode 100644 src/project/document.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fe99a3..56f5c72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,7 @@ list(APPEND SRC_FILES src/gui/storytree src/gui/storytreedelegate src/gui/treetoolbar + src/project/document src/project/project src/project/storyitem src/project/storymodel diff --git a/i18n/collett_en_US.ts b/i18n/collett_en_US.ts index 580f23f..7ed60e4 100644 --- a/i18n/collett_en_US.ts +++ b/i18n/collett_en_US.ts @@ -24,32 +24,37 @@ - + Align Left - + Align Right - + Align Centre - + Align Justify - + + First Line Indent + + + + Indent Paragraph - + Outdent Paragraph @@ -57,7 +62,7 @@ Collett::GuiMain - + %1 %2 Version %3 @@ -98,74 +103,74 @@ Collett::GuiStoryTree - + Rename - + Add Scene - - - - - + + + + + Inside - - - - - + + + + + Before - - - - - + + + + + After - + Add Chapter - + Add Partition - + Add Book - + Here - + Add Page - + Rename Story Item - + New name: @@ -186,7 +191,7 @@ Collett::Project - + Unnamed Project @@ -194,28 +199,28 @@ Collett::Storage - + Could not find or create project storage folder: %1 - - + + Could not open file: %1 - + Could not parse file: %1 - + Unexpected content of file: %1 - + Could not create folder: %1 @@ -228,27 +233,27 @@ - + Book - + Partition - + Chapter - + Scene - + Page diff --git a/i18n/collett_nb_NO.ts b/i18n/collett_nb_NO.ts index 0e098e6..e162ee2 100644 --- a/i18n/collett_nb_NO.ts +++ b/i18n/collett_nb_NO.ts @@ -24,32 +24,37 @@ - + Align Left - + Align Right - + Align Centre - + Align Justify - + + First Line Indent + + + + Indent Paragraph - + Outdent Paragraph @@ -57,7 +62,7 @@ Collett::GuiMain - + %1 %2 Version %3 @@ -98,74 +103,74 @@ Collett::GuiStoryTree - + Rename - + Add Scene - - - - - + + + + + Inside - - - - - + + + + + Before - - - - - + + + + + After - + Add Chapter - + Add Partition - + Add Book - + Here - + Add Page - + Rename Story Item - + New name: @@ -186,7 +191,7 @@ Collett::Project - + Unnamed Project @@ -194,28 +199,28 @@ Collett::Storage - + Could not find or create project storage folder: %1 - - + + Could not open file: %1 - + Could not parse file: %1 - + Unexpected content of file: %1 - + Could not create folder: %1 @@ -228,27 +233,27 @@ - + Book - + Partition - + Chapter - + Scene - + Page diff --git a/sample/content/7e5a1a98-d1a3-44a1-ab4e-2b5d21d92201.json b/sample/content/7e5a1a98-d1a3-44a1-ab4e-2b5d21d92201.json new file mode 100644 index 0000000..79e8c2e --- /dev/null +++ b/sample/content/7e5a1a98-d1a3-44a1-ab4e-2b5d21d92201.json @@ -0,0 +1,72 @@ +{ + "m:created": "2022-01-29T18:22:54", + "m:updated": "2022-01-29T18:23:42", + "x:content": [ + { + "u:fmt": "h3:al", + "u:txt": "t:b|Some Document" + }, + { + "u:fmt": "p:al", + "u:txt": "t:b|Hello World!" + }, + { + "u:fmt": "p:al", + "x:txt": [ + "t|This is a text ", + "t:i|paragraph", + "t| with some ", + "t:u|simple", + "t| formatting." + ] + }, + { + "u:fmt": "p:al:ti", + "x:txt": [ + "t|Here is a paragraph with no formatting whatsoever.\nThis is a second line in the same ", + "t:s|silly", + "t| paragraph." + ] + }, + { + "u:fmt": "p:al", + "u:txt": "t|" + }, + { + "u:fmt": "p:al", + "u:txt": "t|Blank paragraph above here." + }, + { + "u:fmt": "p:ac", + "u:txt": "t|* * *" + }, + { + "u:fmt": "p:al", + "u:txt": "t|This text belongs to a second section of the text document." + }, + { + "u:fmt": "p:al", + "u:txt": "t|" + }, + { + "u:fmt": "p:al", + "u:txt": "t:b|Song time!" + }, + { + "u:fmt": "p:al", + "u:txt": "t|I am the very model of a modern Major-General\nI've information vegetable, animal, and mineral\nI know the kings of England, and I quote the fights historical\nFrom Marathon to Waterloo, in order categorical" + }, + { + "u:fmt": "p:al", + "u:txt": "t|I'm very well acquainted, too, with matters mathematical\nI understand equations, both the simple and quadratical\nAbout binomial theorem I'm teeming with a lot o’ news\nWith many cheerful facts about the square of the hypotenuse" + }, + { + "u:fmt": "p:al:in1", + "u:txt": "t|With many cheerful facts about the square of the hypotenuse\nWith many cheerful facts about the square of the hypotenuse\nWith many cheerful facts about the square of the hypotepotenuse" + }, + { + "u:fmt": "p:al", + "u:txt": "t|" + } + ] +} diff --git a/sample/content/e709ba3f-3141-4b4b-95df-4a8d3e91a8ba.json b/sample/content/e709ba3f-3141-4b4b-95df-4a8d3e91a8ba.json new file mode 100644 index 0000000..db7530b --- /dev/null +++ b/sample/content/e709ba3f-3141-4b4b-95df-4a8d3e91a8ba.json @@ -0,0 +1,10 @@ +{ + "m:created": "2022-01-30T18:54:49", + "m:updated": "2022-01-30T18:58:53", + "x:content": [ + { + "u:fmt": "p:ac", + "u:txt": "t|My Novel" + } + ] +} diff --git a/sample/project/project.json b/sample/project/project.json index 4b474fb..6cb74de 100644 --- a/sample/project/project.json +++ b/sample/project/project.json @@ -1,11 +1,12 @@ { - "c:meta": { - "m:created": "2021-12-14T22:24:25", - "m:updated": "2022-01-23T18:46:08" - }, - "c:project": { - "u:project-name": "Sample Project" - }, - "c:settings": { - } + "c:meta": { + "m:created": "2021-12-14T22:24:25", + "m:updated": "2022-01-30T18:58:56" + }, + "c:project": { + "s:last-doc-main": "7e5a1a98-d1a3-44a1-ab4e-2b5d21d92201", + "u:project-name": "Sample Project" + }, + "c:settings": { + } } diff --git a/sample/project/story.json b/sample/project/story.json index e80202d..669ba80 100644 --- a/sample/project/story.json +++ b/sample/project/story.json @@ -1,74 +1,74 @@ { - "u:type": "ROOT", - "x:items": [ - { - "m:handle": "e709ba3f-3141-4b4b-95df-4a8d3e91a8ba", - "m:order": 0, - "m:words": 1234, - "u:name": "Novel", - "u:type": "BOOK", - "x:items": [ - { - "m:handle": "c2290a95-ca41-4181-89f2-18cb610ab716", - "m:order": 0, - "m:words": 1234, - "u:name": "Title Page", - "u:type": "PAGE" - }, - { - "m:handle": "af3dc4a4-5f66-462d-b63b-9c4d7a7dec77", - "m:order": 1, - "m:words": 1234, - "u:name": "Chapter 1", - "u:type": "CHAPTER", - "x:items": [ - { - "m:handle": "63913842-121f-43ed-8d9c-b3e2432599ab", - "m:order": 0, - "m:words": 1234, - "u:name": "Scene 1.1", - "u:type": "SCENE" - }, - { - "m:handle": "7e5a1a98-d1a3-44a1-ab4e-2b5d21d92201", - "m:order": 1, - "m:words": 1234, - "u:name": "Scene 1.2", - "u:type": "SCENE" - } - ] - }, - { - "m:handle": "61fc6238-87cf-4b78-b2c7-a430776a2b7a", - "m:order": 2, - "m:words": 1234, - "u:name": "Chapter 2", - "u:type": "CHAPTER", - "x:items": [ - { - "m:handle": "f3993444-4e43-4ace-bfdb-55ca659ca914", - "m:order": 0, - "m:words": 1234, - "u:name": "Scene 2.1", - "u:type": "SCENE" - }, - { - "m:handle": "ab6ba4f4-5658-4ff0-941d-2e0c687fe88d", - "m:order": 1, - "m:words": 1234, - "u:name": "Scene 2.2", - "u:type": "SCENE" - } - ] - }, - { - "m:handle": "f1dffe99-3583-4f9c-9f0b-1a8f323b0670", - "m:order": 3, - "m:words": 1234, - "u:name": "Very long title on this element here", - "u:type": "PAGE" - } - ] - } - ] + "u:type": "ROOT", + "x:items": [ + { + "m:handle": "e709ba3f-3141-4b4b-95df-4a8d3e91a8ba", + "m:order": 0, + "m:words": 1234, + "u:name": "Novel", + "u:type": "BOOK", + "x:items": [ + { + "m:handle": "c2290a95-ca41-4181-89f2-18cb610ab716", + "m:order": 0, + "m:words": 1234, + "u:name": "Title Page", + "u:type": "PAGE" + }, + { + "m:handle": "af3dc4a4-5f66-462d-b63b-9c4d7a7dec77", + "m:order": 1, + "m:words": 1234, + "u:name": "Chapter 1", + "u:type": "CHAPTER", + "x:items": [ + { + "m:handle": "63913842-121f-43ed-8d9c-b3e2432599ab", + "m:order": 0, + "m:words": 1234, + "u:name": "Scene 1.1", + "u:type": "SCENE" + }, + { + "m:handle": "7e5a1a98-d1a3-44a1-ab4e-2b5d21d92201", + "m:order": 1, + "m:words": 1234, + "u:name": "Scene 1.2", + "u:type": "SCENE" + } + ] + }, + { + "m:handle": "61fc6238-87cf-4b78-b2c7-a430776a2b7a", + "m:order": 2, + "m:words": 1234, + "u:name": "Chapter 2", + "u:type": "CHAPTER", + "x:items": [ + { + "m:handle": "f3993444-4e43-4ace-bfdb-55ca659ca914", + "m:order": 0, + "m:words": 1234, + "u:name": "Scene 2.1", + "u:type": "SCENE" + }, + { + "m:handle": "ab6ba4f4-5658-4ff0-941d-2e0c687fe88d", + "m:order": 1, + "m:words": 1234, + "u:name": "Scene 2.2", + "u:type": "SCENE" + } + ] + }, + { + "m:handle": "f1dffe99-3583-4f9c-9f0b-1a8f323b0670", + "m:order": 3, + "m:words": 1234, + "u:name": "Very long title on this element here", + "u:type": "PAGE" + } + ] + } + ] } diff --git a/src/collett.h b/src/collett.h index 4a22868..88afc0d 100644 --- a/src/collett.h +++ b/src/collett.h @@ -38,7 +38,15 @@ enum DocAction { TextAlignRight, TextAlignJustify, TextIndent, - TextOutdent, + BlockIndent, + BlockOutdent, +}; + +enum Severity { + Information, + Warning, + Error, + Bug, }; } // namespace Collett diff --git a/src/core/icons.cpp b/src/core/icons.cpp index b3ad48c..3410913 100644 --- a/src/core/icons.cpp +++ b/src/core/icons.cpp @@ -43,6 +43,7 @@ CollettIcons *CollettIcons::instance() { void CollettIcons::destroy() { if (staticInstance != nullptr) { + qDebug() << "Destructor: Static CollettIcons"; delete CollettIcons::staticInstance; } } @@ -152,6 +153,11 @@ CollettIcons::CollettIcons() { "85,6H6.19l4.32,6.73L5.88,20z" ); + // subject_black_24dp.svg (rotated 180 degrees) + m_svgPath["textIndent"] = QLatin1String( + "m10 7h10v-2h-10zm-6 8h16v-2h-16zm16-6h-16v2h16zm0 10v-2h-16v2z" + ); + // format_underlined_black_24dp.svg m_svgPath["underline"] = QLatin1String( "M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2" diff --git a/src/core/storage.cpp b/src/core/storage.cpp index 2ef1d2f..bb84d92 100644 --- a/src/core/storage.cpp +++ b/src/core/storage.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -81,7 +82,11 @@ bool Storage::loadFile(const QString &fileName, QJsonObject &fileData) { } bool Storage::loadFile(const QUuid &fileUuid, QJsonObject &fileData) { - return true; + if (!ensureFolder("content")) { + return false; + } + QString filePath = QDir(m_rootPath.path() + "/content").filePath(fileUuid.toString(QUuid::WithoutBraces) + ".json"); + return readJson(filePath, fileData); } bool Storage::saveFile(const QString &fileName, const QJsonObject &fileData) { @@ -93,7 +98,11 @@ bool Storage::saveFile(const QString &fileName, const QJsonObject &fileData) { } bool Storage::saveFile(const QUuid &fileUuid, const QJsonObject &fileData) { - return true; + if (!ensureFolder("content")) { + return false; + } + QString filePath = QDir(m_rootPath.path() + "/content").filePath(fileUuid.toString(QUuid::WithoutBraces) + ".json"); + return writeJson(filePath, fileData); } bool Storage::loadProjectFile() { @@ -149,6 +158,33 @@ bool Storage::saveProjectFile() { } } +QList Storage::listContent() const { + + if (!m_isValid) { + return QList(); + } + + QList result; + + QDir contentPath = QDir(m_rootPath.path() + "/content"); + QStringList jsonFiles = contentPath.entryList(QStringList() << "*.json", QDir::Files); + + for (const QString &entry : jsonFiles) { + if (entry.length() != 41) { + qWarning() << "Unknown content:" << entry; + continue; + } + QUuid uuid(entry.first(36)); + if (uuid.isNull()) { + qWarning() << "Skipped content:" << entry; + continue; + } + result << uuid; + } + + return result; +} + bool Storage::isValid() { return m_isValid; } @@ -218,6 +254,8 @@ bool Storage::readJson(const QString &filePath, QJsonObject &fileData) { } fileData = json.object(); + qDebug() << "Read:" << filePath; + return true; } @@ -231,8 +269,21 @@ bool Storage::writeJson(const QString &filePath, const QJsonObject &fileData) { } QJsonDocument doc(fileData); - file.write(doc.toJson()); + QByteArray jsonData = doc.toJson(m_compactJson ? QJsonDocument::Compact : QJsonDocument::Indented); + + if (m_compactJson) { + file.write(jsonData); + } else { + for (QByteArray line: jsonData.split('\n')) { + QByteArray trimmed = line.trimmed(); + if (trimmed.length() > 0) { + file.write(QByteArray((line.length() - trimmed.length())/4, '\t') + trimmed + '\n'); + } + } + } file.close(); + qDebug() << "Wrote:" << filePath; + return true; } diff --git a/src/core/storage.h b/src/core/storage.h index 57cc20e..2c9cfda 100644 --- a/src/core/storage.h +++ b/src/core/storage.h @@ -23,6 +23,7 @@ #define COLLETT_STORAGE_H #include +#include #include #include #include @@ -50,6 +51,8 @@ class Storage : public QObject bool loadProjectFile(); bool saveProjectFile(); + QList listContent() const; + bool isValid(); QString projectPath() const; bool hasError(); diff --git a/src/editor/doceditor.cpp b/src/editor/doceditor.cpp index 2182afd..f797fac 100644 --- a/src/editor/doceditor.cpp +++ b/src/editor/doceditor.cpp @@ -22,20 +22,29 @@ #include "collett.h" #include "doceditor.h" #include "textedit.h" +#include "document.h" #include "edittoolbar.h" +#include +#include +#include #include #include +#include #include +#include namespace Collett { GuiDocEditor::GuiDocEditor(QWidget *parent) : QWidget(parent) { + m_data = CollettData::instance(); + m_docUuid = QUuid(); + m_document = nullptr; + m_textArea = new GuiTextEdit(this); m_editToolBar = new GuiEditToolBar(this); - connect(m_editToolBar, SIGNAL(documentAction(DocAction)), m_textArea, SLOT(applyDocAction(DocAction))); QVBoxLayout *outerBox = new QVBoxLayout; outerBox->addWidget(m_editToolBar); @@ -44,8 +53,92 @@ GuiDocEditor::GuiDocEditor(QWidget *parent) outerBox->setSpacing(0); this->setLayout(outerBox); - m_textArea->setHtml("Hello World"); - qDebug() << "QTextDocument:" << sizeof(m_textArea->document()); + + // Connections + + connect(m_editToolBar, SIGNAL(documentAction(DocAction)), + m_textArea, SLOT(applyDocAction(DocAction))); + connect(m_textArea, SIGNAL(currentCharFormatChanged(const QTextCharFormat&)), + this, SLOT(editorCharFormatChanged(const QTextCharFormat&))); + connect(m_textArea, SIGNAL(currentBlockChanged(const QTextBlock&)), + this, SLOT(editorBlockChanged(const QTextBlock&))); +} + +/** + * Data Methods + * ============ + */ + +bool GuiDocEditor::openDocument(const QUuid &uuid) { + + if (!m_data->hasProject()) { + qWarning() << "No project loaded"; + return false; + } + + m_docUuid = uuid; + m_document = m_data->project()->document(uuid); + m_textArea->setJsonContent(m_document->content()); + + return true; +} + +bool GuiDocEditor::saveDocument() { + + if (!m_data->hasProject()) { + qWarning() << "No project loaded"; + return false; + } + if (m_docUuid.isNull()) { + qWarning() << "No document to save"; + return false; + } + QTime startTime = QTime::currentTime(); + m_document->save(m_textArea->toJsonContent()); + QTime endTime = QTime::currentTime(); + qDebug() << "Save file took (ms):" << startTime.msecsTo(endTime); + + return true; +} + +void GuiDocEditor::closeDocument() { + m_textArea->clear(); + m_docUuid = QUuid(); + m_document = nullptr; +} + +/** + * Status Methods + * ============== + */ + +QUuid GuiDocEditor::currentDocument() const { + return m_docUuid; +} + +bool GuiDocEditor::hasDocument() const { + return m_document != nullptr; +} + +/** + * Private Slots + * ============= + */ + +void GuiDocEditor::editorCharFormatChanged(const QTextCharFormat &fmt) { + m_editToolBar->m_formatBold->setChecked(fmt.fontWeight() > QFont::Medium); + m_editToolBar->m_formatItalic->setChecked(fmt.fontItalic()); + m_editToolBar->m_formatUnderline->setChecked(fmt.fontUnderline()); + m_editToolBar->m_formatStrikethrough->setChecked(fmt.fontStrikeOut()); +} + +void GuiDocEditor::editorBlockChanged(const QTextBlock &block) { + QTextBlockFormat blockFormat = block.blockFormat(); + m_editToolBar->m_alignLeft->setChecked(blockFormat.alignment() == Qt::AlignLeft); + m_editToolBar->m_alignCentre->setChecked(blockFormat.alignment() == Qt::AlignHCenter); + m_editToolBar->m_alignRight->setChecked(blockFormat.alignment() == Qt::AlignRight); + m_editToolBar->m_alignJustify->setChecked(blockFormat.alignment() == Qt::AlignJustify); + m_editToolBar->m_textIndent->setChecked(blockFormat.textIndent() > 0.0); } } // namespace Collett diff --git a/src/editor/doceditor.h b/src/editor/doceditor.h index 2d90830..562c81b 100644 --- a/src/editor/doceditor.h +++ b/src/editor/doceditor.h @@ -22,11 +22,17 @@ #ifndef GUI_DOCEDITOR_H #define GUI_DOCEDITOR_H +#include "collett.h" +#include "data.h" #include "textedit.h" +#include "document.h" #include "edittoolbar.h" +#include #include #include +#include +#include namespace Collett { @@ -38,10 +44,32 @@ class GuiDocEditor : public QWidget GuiDocEditor(QWidget *parent=nullptr); ~GuiDocEditor() {}; + // Data Methods + + bool openDocument(const QUuid &uuid); + bool saveDocument(); + void closeDocument(); + + // Status Methods + + QUuid currentDocument() const; + bool hasDocument() const; + +signals: + void popMessage(const Collett::Severity type, const QString &message); + private: GuiTextEdit *m_textArea; GuiEditToolBar *m_editToolBar; + CollettData *m_data; + Document *m_document; + QUuid m_docUuid; + +private slots: + void editorCharFormatChanged(const QTextCharFormat &fmt); + void editorBlockChanged(const QTextBlock &block); + }; } // namespace Collett diff --git a/src/editor/edittoolbar.cpp b/src/editor/edittoolbar.cpp index e08755b..2127844 100644 --- a/src/editor/edittoolbar.cpp +++ b/src/editor/edittoolbar.cpp @@ -43,6 +43,11 @@ GuiEditToolBar::GuiEditToolBar(QWidget *parent) m_formatUnderline = this->addAction(icons->icon("underline"), tr("Underline")); m_formatStrikethrough = this->addAction(icons->icon("strikethrough"), tr("Strikethrough")); + m_formatBold->setCheckable(true); + m_formatItalic->setCheckable(true); + m_formatUnderline->setCheckable(true); + m_formatStrikethrough->setCheckable(true); + connect(m_formatBold, &QAction::triggered, [this]{emitDocumentAction(DocAction::FormatBold);}); connect(m_formatItalic, &QAction::triggered, [this]{emitDocumentAction(DocAction::FormatItalic);}); connect(m_formatUnderline, &QAction::triggered, [this]{emitDocumentAction(DocAction::FormatUnderline);}); @@ -55,6 +60,11 @@ GuiEditToolBar::GuiEditToolBar(QWidget *parent) m_alignRight = this->addAction(icons->icon("alignRight"), tr("Align Centre")); m_alignJustify = this->addAction(icons->icon("alignJustify"), tr("Align Justify")); + m_alignLeft->setCheckable(true); + m_alignCentre->setCheckable(true); + m_alignRight->setCheckable(true); + m_alignJustify->setCheckable(true); + connect(m_alignLeft, &QAction::triggered, [this]{emitDocumentAction(DocAction::TextAlignLeft);}); connect(m_alignCentre, &QAction::triggered, [this]{emitDocumentAction(DocAction::TextAlignCentre);}); connect(m_alignRight, &QAction::triggered, [this]{emitDocumentAction(DocAction::TextAlignRight);}); @@ -62,13 +72,22 @@ GuiEditToolBar::GuiEditToolBar(QWidget *parent) this->addSeparator(); + m_textIndent = this->addAction(icons->icon("textIndent"), tr("First Line Indent")); m_blockIndent = this->addAction(icons->icon("blockIndent"), tr("Indent Paragraph")); m_blockOutdent = this->addAction(icons->icon("blockOutdent"), tr("Outdent Paragraph")); - connect(m_blockIndent, &QAction::triggered, [this]{emitDocumentAction(DocAction::TextIndent);}); - connect(m_blockOutdent, &QAction::triggered, [this]{emitDocumentAction(DocAction::TextOutdent);}); + m_textIndent->setCheckable(true); + + connect(m_textIndent, &QAction::triggered, [this]{emitDocumentAction(DocAction::TextIndent);}); + connect(m_blockIndent, &QAction::triggered, [this]{emitDocumentAction(DocAction::BlockIndent);}); + connect(m_blockOutdent, &QAction::triggered, [this]{emitDocumentAction(DocAction::BlockOutdent);}); } +/** + * Private Slots + * ============= + */ + void GuiEditToolBar::emitDocumentAction(DocAction action) { emit documentAction(action); } diff --git a/src/editor/edittoolbar.h b/src/editor/edittoolbar.h index 5c7d2cc..2d2cf2f 100644 --- a/src/editor/edittoolbar.h +++ b/src/editor/edittoolbar.h @@ -30,6 +30,8 @@ namespace Collett { + +class GuiDocEditor; class GuiEditToolBar : public QToolBar { Q_OBJECT @@ -52,12 +54,15 @@ class GuiEditToolBar : public QToolBar QAction *m_alignRight; QAction *m_alignJustify; + QAction *m_textIndent; QAction *m_blockIndent; QAction *m_blockOutdent; private slots: void emitDocumentAction(DocAction action); + friend class GuiDocEditor; + }; } // namespace Collett diff --git a/src/editor/textedit.cpp b/src/editor/textedit.cpp index ebd6a03..0b00738 100644 --- a/src/editor/textedit.cpp +++ b/src/editor/textedit.cpp @@ -20,20 +20,261 @@ */ #include "collett.h" +#include "settings.h" #include "textedit.h" +#include + +#include #include #include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include namespace Collett { GuiTextEdit::GuiTextEdit(QWidget *parent) : QTextEdit(parent) -{} +{ + // Settings + setAcceptRichText(true); + + // Text Options + QTextOption opts; + opts.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + document()->setDefaultTextOption(opts); + document()->setDocumentMargin(40); + + CollettSettings *settings = CollettSettings::instance(); + m_format = settings->textFormat(); + + connect(this, SIGNAL(cursorPositionChanged()), + this, SLOT(processCursorPositionChanged())); +} + +/** + * Methods + * ======= + */ + +QJsonArray GuiTextEdit::toJsonContent() { + + QJsonArray json; + + QTextBlock block = this->document()->firstBlock(); + while(block.isValid()) { + QJsonObject jsonBlock; + QJsonArray jsonFrags; + QStringList jsonBlockFmt; + + QTextBlockFormat blockFormat = block.blockFormat(); + + // Block Type + if (blockFormat.headingLevel() > 0) { + jsonBlockFmt << QString().setNum(blockFormat.headingLevel()).prepend("h"); + } else { + jsonBlockFmt << "p"; + } + + // Block Alignment + switch (blockFormat.alignment()) { + case Qt::AlignLeading: jsonBlockFmt << "al"; break; + case Qt::AlignCenter: jsonBlockFmt << "ac"; break; + case Qt::AlignHCenter: jsonBlockFmt << "ac"; break; + case Qt::AlignTrailing: jsonBlockFmt << "at"; break; + case Qt::AlignJustify: jsonBlockFmt << "aj"; break; + default: jsonBlockFmt << "al"; break; + } + + // Text Indent + if (blockFormat.textIndent() > 0.0) { + jsonBlockFmt << "ti"; + } + + // Block Indent + if (blockFormat.indent() > 0) { + jsonBlockFmt << QString().setNum(blockFormat.indent()).prepend("in"); + } + + // Write Format + jsonBlock.insert(QLatin1String("u:fmt"), jsonBlockFmt.join(":")); + + // Write Text + QTextBlock::Iterator blockIt = block.begin(); + for (; !blockIt.atEnd(); ++blockIt) { + + QJsonObject jsonFrag; + QTextFragment blockFrag = blockIt.fragment(); + QTextCharFormat fragFmt = blockFrag.charFormat(); + + QStringList jsonFragFmt; + + jsonFragFmt << "t"; + if (fragFmt.fontWeight() > QFont::Medium) jsonFragFmt << "b"; + if (fragFmt.fontItalic()) jsonFragFmt << "i"; + if (fragFmt.fontUnderline()) jsonFragFmt << "u"; + if (fragFmt.fontStrikeOut()) jsonFragFmt << "s"; + + jsonFrags.append(jsonFragFmt.join(":") + "|" + blockFrag.text().replace(QChar::LineSeparator, '\n')); + } + + switch (jsonFrags.size()) { + case 0: + jsonBlock.insert(QLatin1String("u:txt"), "t|"); + break; + case 1: + jsonBlock.insert(QLatin1String("u:txt"), jsonFrags.at(0)); + break; + default: + jsonBlock.insert(QLatin1String("x:txt"), jsonFrags); + break; + } + + // Finish & Next + json.append(jsonBlock); + block = block.next(); + } + + return json; +} + +void GuiTextEdit::setJsonContent(const QJsonArray &json) { + + QTextDocument *doc = this->document(); + QTextCursor cursor = QTextCursor(doc); + bool isFirst = true; + + doc->setUndoRedoEnabled(false); + doc->clear(); + + for (const QJsonValue &jsonBlockValue : json) { + + if (!jsonBlockValue.isObject()) { + qWarning() << "Unexpected content in JSON array. Expected JSON object."; + continue; + } + + QStringList jsonBlockFmt; + QStringList jsonFrags; + QTextBlock newBlock; + + QJsonObject jsonBlock = jsonBlockValue.toObject(); + if (jsonBlock.contains(QLatin1String("u:fmt"))) { + jsonBlockFmt = jsonBlock[QLatin1String("u:fmt")].toString().split(":"); + } + + QTextCharFormat charFormat = m_format.charDefault; + QTextBlockFormat blockFormat = m_format.blockDefault; + + // The first block format entry must describe the block type + if (!jsonBlockFmt.isEmpty()) { + QString blockFmtType = jsonBlockFmt.first(); + if (blockFmtType == "p") { + charFormat = m_format.charParagraph; + blockFormat = m_format.blockParagraph; + } else if (blockFmtType == "h1") { + charFormat = m_format.charHeader1; + blockFormat = m_format.blockHeader1; + } else if (blockFmtType == "h2") { + charFormat = m_format.charHeader2; + blockFormat = m_format.blockHeader2; + } else if (blockFmtType == "h3") { + charFormat = m_format.charHeader3; + blockFormat = m_format.blockHeader3; + } else if (blockFmtType == "h4") { + charFormat = m_format.charHeader3; + blockFormat = m_format.blockHeader4; + } + jsonBlockFmt.removeFirst(); + } + + // The remaining block format entries describe the other format flags + for (const QString &blockFmtTag : jsonBlockFmt) { + if (blockFmtTag == "al") { + blockFormat.setAlignment(Qt::AlignLeading); + } else if (blockFmtTag == "ac") { + blockFormat.setAlignment(Qt::AlignHCenter); + } else if (blockFmtTag == "at") { + blockFormat.setAlignment(Qt::AlignTrailing); + } else if (blockFmtTag == "aj") { + blockFormat.setAlignment(Qt::AlignJustify); + } else if (blockFmtTag == "ti") { + blockFormat.setTextIndent(m_format.textIndent); + } else if (blockFmtTag.startsWith("in")) { + blockFormat.setIndent(blockFmtTag.last(1).toInt()); + } + } + + if (isFirst) { + cursor.setBlockFormat(blockFormat); + isFirst = false; + } else { + cursor.insertBlock(blockFormat); + } + + if (jsonBlock.contains(QLatin1String("u:txt"))) { + jsonFrags << jsonBlock[QLatin1String("u:txt")].toString(); + } else if (jsonBlock.contains(QLatin1String("x:txt"))) { + for (const QJsonValue &jsonFragValue : jsonBlock[QLatin1String("x:txt")].toArray()) { + jsonFrags << jsonFragValue.toString(); + } + } + + for (const QString &fragText : jsonFrags) { + + qsizetype fmtTagPos = fragText.indexOf("|"); + if (fmtTagPos < 0) { + qWarning() << "Could not parse format of text line"; + cursor.insertText(fragText); + continue; + } + + QStringList fragCharFmt = fragText.first(fmtTagPos).split(":"); + QString innerText = fragText.sliced(fmtTagPos + 1).replace('\n', QChar::LineSeparator); + + QTextCharFormat fragFormat = charFormat; + bool isText = false;; + for (const QString &fragFmtTag : fragCharFmt) { + if (fragFmtTag == "t") { + isText = true; + } else if (fragFmtTag == "b") { + fragFormat.setFontWeight(QFont::Bold); + } else if (fragFmtTag == "i") { + fragFormat.setFontItalic(true); + } else if (fragFmtTag == "u") { + fragFormat.setFontUnderline(true); + } else if (fragFmtTag == "s") { + fragFormat.setFontStrikeOut(true); + } + } + + if (isText) { + cursor.insertText(innerText, fragFormat); + } + } + + } + + doc->setUndoRedoEnabled(true); +} + +/** + * Public Slots + * ============ + */ void GuiTextEdit::applyDocAction(DocAction action) { + bool blockChanged = false; + if (action == Collett::FormatBold) { if (fontWeight() > QFont::Medium) { setFontWeight(QFont::Normal); @@ -54,32 +295,71 @@ void GuiTextEdit::applyDocAction(DocAction action) { } else if (action == Collett::TextAlignLeft) { setAlignment(Qt::AlignLeft); + blockChanged = true; } else if (action == Collett::TextAlignCentre) { setAlignment(Qt::AlignHCenter); + blockChanged = true; } else if (action == Collett::TextAlignRight) { setAlignment(Qt::AlignRight); + blockChanged = true; } else if (action == Collett::TextAlignJustify) { setAlignment(Qt::AlignJustify); + blockChanged = true; } else if (action == Collett::TextIndent) { - // Indenting is only allowed on text paragraphs (no heading level) that - // are also aligned to the leading edge. QTextCursor cursor = textCursor(); QTextBlockFormat format = cursor.blockFormat(); if (format.headingLevel() == 0 && format.alignment() == Qt::AlignLeading) { - format.setTextIndent(8.0); + if (format.textIndent() > 0.0) { + format.setTextIndent(0.0); + } else { + format.setTextIndent(m_format.textIndent); + } + cursor.setBlockFormat(format); + } + blockChanged = true; + + } else if (action == Collett::BlockIndent) { + QTextCursor cursor = textCursor(); + QTextBlockFormat format = cursor.blockFormat(); + if (format.headingLevel() == 0 && format.alignment() == Qt::AlignLeading) { + format.setIndent(std::min(format.indent() + 1, 9)); cursor.setBlockFormat(format); } + blockChanged = true; - } else if (action == Collett::TextOutdent) { - // Text outdent is always allowed as there is no need to restrict it. + } else if (action == Collett::BlockOutdent) { QTextCursor cursor = textCursor(); QTextBlockFormat format = cursor.blockFormat(); - format.setTextIndent(0.0); - cursor.setBlockFormat(format); + if (format.headingLevel() == 0 && format.alignment() == Qt::AlignLeading) { + format.setIndent(std::max(format.indent() - 1, 0)); + cursor.setBlockFormat(format); + } + blockChanged = true; + } + + if (blockChanged) { + QTextCursor cursor = textCursor(); + emit currentBlockChanged(cursor.block()); + } +} + +/** + * Private Slots + * ============= + */ + +void GuiTextEdit::processCursorPositionChanged() { + + QTextCursor cursor = textCursor(); + int blockNo = cursor.blockNumber(); + + if (blockNo != m_currentBlockNo) { + emit currentBlockChanged(cursor.block()); + m_currentBlockNo = blockNo; } } diff --git a/src/editor/textedit.h b/src/editor/textedit.h index 302ebbd..54ce014 100644 --- a/src/editor/textedit.h +++ b/src/editor/textedit.h @@ -23,10 +23,16 @@ #define GUI_TEXTEDIT_H #include "collett.h" +#include "settings.h" +#include "data.h" #include #include #include +#include +#include +#include +#include namespace Collett { @@ -38,9 +44,23 @@ class GuiTextEdit : public QTextEdit GuiTextEdit(QWidget *parent=nullptr); ~GuiTextEdit() {}; + QJsonArray toJsonContent(); + void setJsonContent(const QJsonArray &json); + +private: + CollettSettings::TextFormat m_format; + + int m_currentBlockNo = -1; + +signals: + void currentBlockChanged(const QTextBlock &block); + public slots: void applyDocAction(DocAction action); +private slots: + void processCursorPositionChanged(); + }; } // namespace Collett diff --git a/src/gui/storytree.cpp b/src/gui/storytree.cpp index 3cdae87..b7ee39f 100644 --- a/src/gui/storytree.cpp +++ b/src/gui/storytree.cpp @@ -52,16 +52,19 @@ GuiStoryTree::GuiStoryTree(QWidget *parent) this->setItemDelegate(new GuiStoryTreeDelegate(this)); this->setHeaderHidden(true); this->setAlternatingRowColors(true); + this->setExpandsOnDoubleClick(false); // Item Actions m_editItem = new QAction(tr("Rename"), this); m_editItem->setShortcut(QKeySequence("F2")); - connect(m_editItem, SIGNAL(triggered(bool)), this, SLOT(doEditName(bool))); this->addAction(m_editItem); + connect(m_editItem, SIGNAL(triggered(bool)), + this, SLOT(doEditName(bool))); // Connect the Context Menu this->setContextMenuPolicy(Qt::CustomContextMenu); - connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(doOpenContextMenu(QPoint))); + connect(this, SIGNAL(customContextMenuRequested(QPoint)), + this, SLOT(doOpenContextMenu(QPoint))); } /** diff --git a/src/guimain.cpp b/src/guimain.cpp index 03d472d..9d87add 100644 --- a/src/guimain.cpp +++ b/src/guimain.cpp @@ -29,10 +29,12 @@ #include "storytree.h" #include "treetoolbar.h" +#include #include #include #include #include +#include #include #include @@ -67,6 +69,10 @@ GuiMain::GuiMain(QWidget *parent) : QMainWindow(parent) { resize(mainConf->mainWindowSize()); m_splitMain->setSizes(mainConf->mainSplitSizes()); + // Connect Signals and Slots + connect(m_storyTree, SIGNAL(doubleClicked(const QModelIndex&)), + this, SLOT(storyTreeDoubleClick(const QModelIndex&))); + // Finalise setWindowTitle( tr("%1 %2 Version %3").arg(qApp->applicationName(), "–", qApp->applicationVersion()) @@ -81,8 +87,8 @@ GuiMain::~GuiMain() { } /** - * Project Functions - * ================= + * Project Methods + * =============== */ void GuiMain::openProject(const QString &path) { @@ -96,11 +102,17 @@ void GuiMain::openProject(const QString &path) { m_storyTree->setTreeModel(m_data->storyModel()); delete m; + QUuid lastDocMain = m_data->project()->lastDocumentMain(); + if (!lastDocMain.isNull()) { + this->openDocument(lastDocMain); + } + m_mainToolBar->setProjectName(m_data->project()->projectName()); }; bool GuiMain::saveProject() { - return m_data->saveProject(); + this->saveDocument(); + return true; } bool GuiMain::closeProject() { @@ -108,12 +120,46 @@ bool GuiMain::closeProject() { }; /** - * GUI Functions - * ============= + * Document Methods + * ================ + */ + +void GuiMain::openDocument(const QUuid &uuid) { + + if (!m_data->hasProject()) { + return; + } + + if (m_docEditor->hasDocument()) { + m_docEditor->saveDocument(); + m_docEditor->closeDocument(); + } + m_docEditor->openDocument(uuid); + m_data->project()->setLastDocumentMain(m_docEditor->currentDocument()); +} + +void GuiMain::saveDocument() { + + if (!m_data->hasProject()) { + return; + } + if (m_docEditor->hasDocument()) { + m_docEditor->saveDocument(); + } +} + +void GuiMain::closeDocument() { + m_docEditor->closeDocument(); +} + +/** + * GUI Methods + * =========== */ bool GuiMain::closeMain() { + m_docEditor->saveDocument(); m_data->saveProject(); m_data->closeProject(); @@ -144,4 +190,16 @@ void GuiMain::closeEvent(QCloseEvent *event) { } } +/** + * Private Slots + * ============= + */ + +void GuiMain::storyTreeDoubleClick(const QModelIndex &index) { + if (!m_data->hasProject() || !index.isValid()) { + return; + } + this->openDocument(m_data->project()->storyModel()->itemHandle(index)); +} + } // namespace Collett diff --git a/src/guimain.h b/src/guimain.h index 4b95ab9..e760f2c 100644 --- a/src/guimain.h +++ b/src/guimain.h @@ -30,11 +30,13 @@ #include "storytree.h" #include "doceditor.h" -#include +#include #include +#include #include #include -#include +#include +#include namespace Collett { @@ -46,10 +48,20 @@ class GuiMain : public QMainWindow GuiMain(QWidget *parent=nullptr); ~GuiMain(); + // Project Methods + void openProject(const QString &path); bool saveProject(); bool closeProject(); + // Document Methods + + void openDocument(const QUuid &uuid); + void saveDocument(); + void closeDocument(); + + // GUI Methods + bool closeMain(); private: @@ -68,6 +80,10 @@ class GuiMain : public QMainWindow // Events void closeEvent(QCloseEvent*); +private slots: + + void storyTreeDoubleClick(const QModelIndex &index); + }; } // namespace Collett diff --git a/src/project/document.cpp b/src/project/document.cpp new file mode 100644 index 0000000..0ec1c79 --- /dev/null +++ b/src/project/document.cpp @@ -0,0 +1,144 @@ +/* +** Collett – Document Class +** ======================== +** +** This file is a part of Collett +** Copyright 2020–2022, Veronica Berglyd Olsen +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, but +** WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "collett.h" +#include "document.h" + +#include +#include +#include +#include + +namespace Collett { + +Document::Document(Storage *store, const QUuid uuid, Document::Mode mode) + : m_store(store), m_handle(uuid) +{ + m_empty = true; + m_existing = false; + m_unsaved = true; + m_mode = mode; + m_created = QDateTime::currentDateTime().toString(Qt::ISODate); +} + +/** + * Class Getters + * ============= + */ + +bool Document::isEmpty() const { + return m_empty; +} + +bool Document::isExisting() const { + return m_existing; +} + +bool Document::isUnsaved() const { + return m_unsaved; +} + +QJsonArray Document::content() const { + return m_content; +} + +QUuid Document::handle() const { + return m_handle; +} + +/** + * Class Methods + * ============= + */ + +/**! + * @brief Open the document and read the content into the data buffer. + * + * @param mode either ReadOnly or ReadWrite. + * @return true if successful, otherwise false. + */ +bool Document::open(const Document::Mode mode) { + + QJsonObject json; + + m_mode = mode; + + if (!m_store->loadFile(m_handle, json)) { + return false; + } + + if (json.contains(QLatin1String("m:created"))) { + m_created = json.value(QLatin1String("m:created")).toString(); + } else { + m_created = "unknown"; + } + if (json.contains(QLatin1String("x:content"))) { + m_content = json.value(QLatin1String("x:content")).toArray(); + } else { + m_content = QJsonArray(); + } + + m_empty = false; + m_existing = true; + + return true; +} + +/**! + * @brief Update content and save data. + * + * This is a convenience function. + * + * @param content the updated document content. + * @return true if successful, otherwise false. + */ +bool Document::save(const QJsonArray &content) { + m_content = content; + m_empty = false; + return save(); +} + +/**! + * @brief Save the content in the data buffer to the document file. + * + * @return true if successful, otherwise false. + */ +bool Document::save() { + + if (m_mode != Document::ReadWrite || m_empty) { + return false; + } + + QJsonObject json; + json.insert(QLatin1String("m:created"), m_created); + json.insert(QLatin1String("m:updated"), QDateTime::currentDateTime().toString(Qt::ISODate)); + json.insert(QLatin1String("x:content"), m_content); + + if (!m_store->saveFile(m_handle, json)) { + return false; + } + + m_existing = true; + m_unsaved = false; + return true; +} + +} // namespace Collett diff --git a/src/project/document.h b/src/project/document.h new file mode 100644 index 0000000..33a3a86 --- /dev/null +++ b/src/project/document.h @@ -0,0 +1,74 @@ +/* +** Collett – Document Class +** ======================== +** +** This file is a part of Collett +** Copyright 2020–2022, Veronica Berglyd Olsen +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, but +** WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#ifndef COLLETT_DOCUMENT_H +#define COLLETT_DOCUMENT_H + +#include "collett.h" +#include "storage.h" + +#include +#include +#include + +namespace Collett { + +class Document : public QObject +{ + Q_OBJECT + +public: + enum Mode {ReadOnly, ReadWrite}; + + explicit Document(Storage *store, const QUuid uuid, Mode mode=Mode::ReadOnly); + ~Document() {}; + + // Getters + + bool isEmpty() const; + bool isExisting() const; + bool isUnsaved() const; + QJsonArray content() const; + QUuid handle() const; + + // Methods + + bool open(const Mode mode); + bool save(const QJsonArray &content); + bool save(); + +private: + Storage *m_store; + QUuid m_handle; + bool m_empty; + bool m_existing; + bool m_unsaved; + Mode m_mode; + + // Data Variables + + QString m_created; + QJsonArray m_content; + +}; +} // namespace Collett + +#endif // COLLETT_DOCUMENT_H diff --git a/src/project/project.cpp b/src/project/project.cpp index 50ef77b..bc878ef 100644 --- a/src/project/project.cpp +++ b/src/project/project.cpp @@ -22,6 +22,7 @@ #include "collett.h" #include "project.h" #include "storage.h" +#include "document.h" #include "storymodel.h" #include @@ -52,9 +53,9 @@ Project::Project(const QString &path) { // If the path is a file, go one level up QFileInfo fObj(path); if (fObj.isFile()) { - m_store = new Storage(fObj.dir().path(), Storage::Folder); + m_store = new Storage(fObj.dir().path(), Storage::Folder, false); } else { - m_store = new Storage(path, Storage::Folder); + m_store = new Storage(path, Storage::Folder, false); } qDebug() << "Project Path:" << m_store->projectPath(); @@ -82,6 +83,7 @@ bool Project::openProject() { if (main) { bool settings = loadSettingsFile(); bool story = loadStoryFile(); + loadContent(); m_isValid = settings && story; } else { m_isValid = false; @@ -101,6 +103,7 @@ bool Project::saveProject() { bool main = m_store->saveProjectFile(); bool settings = saveSettingsFile(); bool story = saveStoryFile(); + saveContent(); return main && settings && story; } @@ -114,6 +117,10 @@ bool Project::isValid() const { * ============= */ +void Project::setLastDocumentMain(const QUuid &uuid) { + m_lastDocMain = uuid; +} + void Project::setProjectName(const QString &name) { m_projectName = name.simplified(); } @@ -123,6 +130,10 @@ void Project::setProjectName(const QString &name) { * ============= */ +QUuid Project::lastDocumentMain() const { + return m_lastDocMain; +} + QString Project::projectName() const { return m_projectName; } @@ -131,6 +142,21 @@ StoryModel *Project::storyModel() { return m_storyModel; } +Storage *Project::store() { + return m_store; +} + +Document *Project::document(const QUuid &uuid) { + if (m_content.contains(uuid)) { + return m_content.value(uuid); + } else { + qDebug() << "Created new document entry for" << uuid.toString(QUuid::WithoutBraces); + Document *doc = new Document(m_store, uuid, Document::ReadWrite); + m_content.insert(uuid, doc); + return doc; + } +} + /** * Settings File * ============= @@ -152,7 +178,13 @@ bool Project::loadSettingsFile() { QJsonObject jProject = jData[QLatin1String("c:project")].toObject(); QJsonObject jSettings = jData[QLatin1String("c:settings")].toObject(); + // Project Meta m_createdTime = Storage::getJsonString(jMeta, QLatin1String("m:created"), "Unknown"); + + // Project State + m_lastDocMain = QUuid(Storage::getJsonString(jProject, QLatin1String("s:last-doc-main"), "")); + + // Project Settings m_projectName = Storage::getJsonString(jProject, QLatin1String("u:project-name"), tr("Unnamed Project")); return true; @@ -162,9 +194,14 @@ bool Project::saveSettingsFile() { QJsonObject jData, jMeta, jProject, jSettings; + // Project Meta jMeta[QLatin1String("m:created")] = m_createdTime; jMeta[QLatin1String("m:updated")] = QDateTime::currentDateTime().toString(Qt::ISODate); + // Project State + jProject[QLatin1String("s:last-doc-main")] = m_lastDocMain.toString(QUuid::WithoutBraces); + + // Project Settings jProject[QLatin1String("u:project-name")] = m_projectName; jData[QLatin1String("c:meta")] = jMeta; @@ -206,6 +243,33 @@ bool Project::saveStoryFile() { return true; } +void Project::loadContent() { + + if (!m_content.isEmpty()) { + qWarning() << "Project content already loaded"; + return; + } + + QList contentList = m_store->listContent(); + for (const QUuid &uuid : contentList) { + qDebug() << "Loading content:" << uuid.toString(QUuid::WithoutBraces); + Document *doc = new Document(m_store, uuid); + if (doc->open(Document::ReadWrite)) { + m_content.insert(uuid, doc); + } + } +} + +void Project::saveContent() { + + for (Document *doc : m_content) { + if (doc->isUnsaved()) { + qDebug() << "Saving content:" << doc->handle().toString(QUuid::WithoutBraces); + doc->save(); + } + } +} + /** * Error Handling * ============== diff --git a/src/project/project.h b/src/project/project.h index c9578d0..8ccbb3b 100644 --- a/src/project/project.h +++ b/src/project/project.h @@ -24,9 +24,12 @@ #include "collett.h" #include "storage.h" +#include "document.h" #include "storymodel.h" #include +#include +#include #include #include @@ -37,7 +40,7 @@ class Project : public QObject Q_OBJECT public: - Project(const QString &path); + explicit Project(const QString &path); ~Project(); // Class Methods @@ -48,12 +51,18 @@ class Project : public QObject // Class Setters + void setLastDocumentMain(const QUuid &uuid); void setProjectName(const QString &name); // Class Getters + QUuid lastDocumentMain() const; + QString projectName() const; StoryModel *storyModel(); + Storage *store(); + + Document *document(const QUuid &uuid); // Error Handling @@ -61,8 +70,8 @@ class Project : public QObject QString lastError() const; private: - bool m_isValid; - QString m_lastError; + bool m_isValid; + QString m_lastError; Storage *m_store; // Project Meta @@ -71,13 +80,18 @@ class Project : public QObject QString m_projectVersion = ""; QString m_createdTime = ""; - // Project Details + // Project State + + QUuid m_lastDocMain; + + // Project Settings QString m_projectName = "New Project"; // Content StoryModel *m_storyModel; + QHash m_content; // File Load & Save @@ -86,6 +100,9 @@ class Project : public QObject bool loadStoryFile(); bool saveStoryFile(); + void loadContent(); + void saveContent(); + }; } // namespace Collett diff --git a/src/project/storyitem.cpp b/src/project/storyitem.cpp index 88b2d29..ce02d0d 100644 --- a/src/project/storyitem.cpp +++ b/src/project/storyitem.cpp @@ -278,6 +278,10 @@ StoryItem::ItemType StoryItem::type() const { return m_type; } +QUuid StoryItem::handle() const { + return m_handle; +} + QString StoryItem::name() const { return m_name; } diff --git a/src/project/storyitem.h b/src/project/storyitem.h index 8621b06..0a1d5ef 100644 --- a/src/project/storyitem.h +++ b/src/project/storyitem.h @@ -56,6 +56,7 @@ class StoryItem : public QObject // Class Getters ItemType type() const; + QUuid handle() const; QString name() const; int wordCount() const; int childWordCounts() const; diff --git a/src/project/storymodel.cpp b/src/project/storymodel.cpp index aa1a985..532d57d 100644 --- a/src/project/storymodel.cpp +++ b/src/project/storymodel.cpp @@ -183,6 +183,15 @@ StoryItem *StoryModel::storyItem(const QModelIndex &index) { } } +QUuid StoryModel::itemHandle(const QModelIndex &index) { + if (index.isValid()) { + StoryItem *item = static_cast(index.internalPointer()); + return item->handle(); + } else { + return QUuid(); + } +} + QString StoryModel::itemName(const QModelIndex &index) { if (index.isValid()) { StoryItem *item = static_cast(index.internalPointer()); diff --git a/src/project/storymodel.h b/src/project/storymodel.h index c6688f9..5d5c9f1 100644 --- a/src/project/storymodel.h +++ b/src/project/storymodel.h @@ -53,6 +53,7 @@ class StoryModel : public QAbstractItemModel StoryItem *rootItem() const; StoryItem *storyItem(const QModelIndex &index); + QUuid itemHandle(const QModelIndex &index); QString itemName(const QModelIndex &index); // Model Edit diff --git a/src/settings.cpp b/src/settings.cpp index 78ee1ea..02cbb69 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -25,32 +25,19 @@ #define CNF_MAIN_WINDOW_SIZE "GuiMain/windowSize" #define CNF_MAIN_SPLIT_SIZES "GuiMain/mainSplitSizes" +#define CNF_TEXT_FONT_SIZE "TextFormat/fontSize" + #include #include #include #include #include +#include #include +#include namespace Collett { -/** - * Private Class Declaration - * ========================= - */ - -class CollettSettingsPrivate -{ -public: - static CollettSettings *instance; - - CollettSettingsPrivate() {}; - ~CollettSettingsPrivate() {}; - - QSize m_mainWindowSize; - QList m_mainSplitSizes; -}; - /** * Converter Functions * =================== @@ -73,44 +60,56 @@ QVariantList intListToVariant(const QList &list) { } /** - * Public Class Contructor/Destructor - * ================================== + * Class Constructor/Destructor/Instance + * ===================================== */ -CollettSettings *CollettSettingsPrivate::instance = nullptr; - +CollettSettings *CollettSettings::staticInstance = nullptr; CollettSettings *CollettSettings::instance() { - if (CollettSettingsPrivate::instance == nullptr) { - CollettSettingsPrivate::instance = new CollettSettings(); + if (staticInstance == nullptr) { + staticInstance = new CollettSettings(); qDebug() << "Constructor: CollettSettings"; } - return CollettSettingsPrivate::instance; + return staticInstance; } void CollettSettings::destroy() { - if (CollettSettingsPrivate::instance != nullptr) { - delete CollettSettingsPrivate::instance; + if (staticInstance != nullptr) { + qDebug() << "Destructor: Static CollettSettings"; + delete CollettSettings::staticInstance; } } -CollettSettings::CollettSettings() - : d_ptr(new CollettSettingsPrivate()) -{ - Q_D(CollettSettings); +CollettSettings::CollettSettings() { // Load Settings QSettings settings; - d->m_mainWindowSize = settings.value(CNF_MAIN_WINDOW_SIZE, QSize(1200, 800)).toSize(); - d->m_mainSplitSizes = variantListToInt(settings.value(CNF_MAIN_SPLIT_SIZES, QVariantList() << 300 << 700).toList()); + // GUI Settings + // ------------ + + m_mainWindowSize = settings.value(CNF_MAIN_WINDOW_SIZE, QSize(1200, 800)).toSize(); + m_mainSplitSizes = variantListToInt(settings.value(CNF_MAIN_SPLIT_SIZES, QVariantList() << 300 << 700).toList()); // Check Values - if (d->m_mainWindowSize.width() < 400) { - d->m_mainWindowSize.setWidth(400); + if (m_mainWindowSize.width() < 400) { + m_mainWindowSize.setWidth(400); + } + if (m_mainWindowSize.height() < 300) { + m_mainWindowSize.setHeight(300); } - if (d->m_mainWindowSize.height() < 300) { - d->m_mainWindowSize.setHeight(300); + + // Text Format + // ----------- + + m_textFontSize = settings.value(CNF_TEXT_FONT_SIZE, (qreal)13.0).toReal(); + + // Check Values + if (m_textFontSize < 5.0) { + m_textFontSize = 5.0; } + recalculateTextFormats(); + } CollettSettings::~CollettSettings() { @@ -123,12 +122,13 @@ CollettSettings::~CollettSettings() { */ void CollettSettings::flushSettings() { - Q_D(CollettSettings); QSettings settings; - settings.setValue(CNF_MAIN_WINDOW_SIZE, d->m_mainWindowSize); - settings.setValue(CNF_MAIN_SPLIT_SIZES, intListToVariant(d->m_mainSplitSizes)); + settings.setValue(CNF_MAIN_WINDOW_SIZE, m_mainWindowSize); + settings.setValue(CNF_MAIN_SPLIT_SIZES, intListToVariant(m_mainSplitSizes)); + + settings.setValue(CNF_TEXT_FONT_SIZE, m_textFontSize); qDebug() << "CollettSettings values saved"; @@ -141,13 +141,16 @@ void CollettSettings::flushSettings() { */ void CollettSettings::setMainWindowSize(const QSize size) { - Q_D(CollettSettings); - d->m_mainWindowSize = size; + m_mainWindowSize = size; } void CollettSettings::setMainSplitSizes(const QList &sizes) { - Q_D(CollettSettings); - d->m_mainSplitSizes = sizes; + m_mainSplitSizes = sizes; +} + +void CollettSettings::setTextFontSize(const qreal size) { + m_textFontSize = size; + recalculateTextFormats(); } /** @@ -156,13 +159,105 @@ void CollettSettings::setMainSplitSizes(const QList &sizes) { */ QSize CollettSettings::mainWindowSize() const { - Q_D(const CollettSettings); - return d->m_mainWindowSize; + return m_mainWindowSize; } QList CollettSettings::mainSplitSizes() const { - Q_D(const CollettSettings); - return d->m_mainSplitSizes; + return m_mainSplitSizes; +} + +CollettSettings::TextFormat CollettSettings::textFormat() const { + return m_textFormat; +} + +/** + * Internal Functions + * ================== + */ + +void CollettSettings::recalculateTextFormats() { + + // Text Formats + + QTextCharFormat defaultCharFmt; + QTextBlockFormat defaultBlockFmt; + + // Default Values + + qreal defaultLineHeight = 1.15; + qreal defaultTopMargin = 0.5 * m_textFontSize; + qreal defaultBottomMargin = 0.5 * m_textFontSize; + + qreal header1FontSize = 2.2*m_textFontSize; + qreal header2FontSize = 1.9*m_textFontSize; + qreal header3FontSize = 1.6*m_textFontSize; + qreal header4FontSize = 1.3*m_textFontSize; + + qreal headerBottomMargin = 0.7 * m_textFontSize; + + // Text Format Values + + m_textFormat.fontSize = m_textFontSize; + m_textFormat.textIndent = 2.0 * m_textFontSize; + m_textFormat.lineHeight = 1.15; + + // Default Text Formats + + defaultBlockFmt.setHeadingLevel(0); + defaultBlockFmt.setLineHeight(defaultLineHeight, QTextBlockFormat::SingleHeight); + defaultBlockFmt.setTopMargin(defaultTopMargin); + defaultBlockFmt.setBottomMargin(defaultBottomMargin); + defaultBlockFmt.setTextIndent(0.0); + m_textFormat.blockDefault = defaultBlockFmt; + + defaultCharFmt.setFontPointSize(m_textFontSize); + m_textFormat.charDefault = defaultCharFmt; + + // Paragraph Formats + + m_textFormat.blockParagraph = defaultBlockFmt; + m_textFormat.charParagraph = defaultCharFmt; + + // Header 1 Formats + + m_textFormat.blockHeader1 = defaultBlockFmt; + m_textFormat.blockHeader1.setHeadingLevel(1); + m_textFormat.blockHeader1.setTopMargin(header1FontSize); + m_textFormat.blockHeader1.setBottomMargin(headerBottomMargin); + + m_textFormat.charHeader1 = defaultCharFmt; + m_textFormat.charHeader1.setFontPointSize(header1FontSize); + + // Header 2 Formats + + m_textFormat.blockHeader2 = defaultBlockFmt; + m_textFormat.blockHeader2.setHeadingLevel(2); + m_textFormat.blockHeader2.setTopMargin(header2FontSize); + m_textFormat.blockHeader2.setBottomMargin(headerBottomMargin); + + m_textFormat.charHeader2 = defaultCharFmt; + m_textFormat.charHeader2.setFontPointSize(header2FontSize); + + // Header 3 Formats + + m_textFormat.blockHeader3 = defaultBlockFmt; + m_textFormat.blockHeader3.setHeadingLevel(3); + m_textFormat.blockHeader3.setTopMargin(header3FontSize); + m_textFormat.blockHeader3.setBottomMargin(headerBottomMargin); + + m_textFormat.charHeader3 = defaultCharFmt; + m_textFormat.charHeader3.setFontPointSize(header3FontSize); + + // Header 4 Formats + + m_textFormat.blockHeader4 = defaultBlockFmt; + m_textFormat.blockHeader4.setHeadingLevel(4); + m_textFormat.blockHeader4.setTopMargin(header4FontSize); + m_textFormat.blockHeader4.setBottomMargin(headerBottomMargin); + + m_textFormat.charHeader4 = defaultCharFmt; + m_textFormat.charHeader4.setFontPointSize(header4FontSize); + } } // namespace Collett diff --git a/src/settings.h b/src/settings.h index 8918d24..397d490 100644 --- a/src/settings.h +++ b/src/settings.h @@ -25,37 +25,74 @@ #include "collett.h" #include +#include #include #include -#include +#include +#include namespace Collett { -class CollettSettingsPrivate; class CollettSettings : public QObject { Q_OBJECT - Q_DECLARE_PRIVATE(CollettSettings) public: + struct TextFormat { + QTextBlockFormat blockDefault; + QTextCharFormat charDefault; + QTextBlockFormat blockParagraph; + QTextCharFormat charParagraph; + QTextBlockFormat blockHeader1; + QTextCharFormat charHeader1; + QTextBlockFormat blockHeader2; + QTextCharFormat charHeader2; + QTextBlockFormat blockHeader3; + QTextCharFormat charHeader3; + QTextBlockFormat blockHeader4; + QTextCharFormat charHeader4; + qreal fontSize; + qreal textIndent; + qreal lineHeight; + }; + static CollettSettings *instance(); static void destroy(); - ~CollettSettings(); -private: - QScopedPointer d_ptr; - CollettSettings(); + explicit CollettSettings(); + ~CollettSettings(); -public: void flushSettings(); // Setters + void setMainWindowSize(const QSize size); void setMainSplitSizes(const QList &sizes); + void setTextFontSize(const qreal size); // Getters + QSize mainWindowSize() const; QList mainSplitSizes() const; + TextFormat textFormat() const; + +private: + static CollettSettings *staticInstance; + + // GUI Settings + + QSize m_mainWindowSize; + QList m_mainSplitSizes; + + // Text Format + + qreal m_textFontSize; + TextFormat m_textFormat; + + // Internal Functions + + void recalculateTextFormats(); + }; } // namespace Collett