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 @@
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
-
+
@@ -57,7 +62,7 @@
Collett::GuiMain
-
+
@@ -98,74 +103,74 @@
Collett::GuiStoryTree
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -186,7 +191,7 @@
Collett::Project
-
+
@@ -194,28 +199,28 @@
Collett::Storage
-
+
-
-
+
+
-
+
-
+
-
+
@@ -228,27 +233,27 @@
-
+
-
+
-
+
-
+
-
+
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 @@
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
-
+
@@ -57,7 +62,7 @@
Collett::GuiMain
-
+
@@ -98,74 +103,74 @@
Collett::GuiStoryTree
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -186,7 +191,7 @@
Collett::Project
-
+
@@ -194,28 +199,28 @@
Collett::Storage
-
+
-
-
+
+
-
+
-
+
-
+
@@ -228,27 +233,27 @@
-
+
-
+
-
+
-
+
-
+
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