From 3db7b94bade759fd922d0ddeede72e8e5123ad32 Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Tue, 13 Oct 2015 19:33:46 +0200
Subject: [PATCH 01/74] [#242] Make order of HTML Preview/Source toggle more
sensible
---
app/mainwindow.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp
index 52d06008..cb967437 100644
--- a/app/mainwindow.cpp
+++ b/app/mainwindow.cpp
@@ -735,7 +735,7 @@ void MainWindow::toggleHtmlView()
if (viewLabel->text() == tr("HTML preview")) {
ui->stackedWidget->setCurrentWidget(ui->htmlSourcePage);
- ui->actionHtmlPreview->setText(tr("HTML source"));
+ ui->actionHtmlPreview->setText(tr("HTML preview"));
viewLabel->setText(tr("HTML source"));
// activate HTML highlighter
@@ -744,7 +744,7 @@ void MainWindow::toggleHtmlView()
} else {
ui->stackedWidget->setCurrentWidget(ui->webViewPage);
- ui->actionHtmlPreview->setText(tr("HTML preview"));
+ ui->actionHtmlPreview->setText(tr("HTML source"));
viewLabel->setText(tr("HTML preview"));
// deactivate HTML highlighter
From 1b1bbccebb0f3cbfa989e19f8104d366ab212b5c Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Tue, 13 Oct 2015 19:35:53 +0200
Subject: [PATCH 02/74] [#241] Add missing mermaid.css to improve mermaid
output
---
app-static/template/htmltemplate.cpp | 1 +
app/resources.qrc | 1 +
app/scripts/mermaid/mermaid.css | 256 +++++++++++++++++++++++++++
3 files changed, 258 insertions(+)
create mode 100644 app/scripts/mermaid/mermaid.css
diff --git a/app-static/template/htmltemplate.cpp b/app-static/template/htmltemplate.cpp
index f7a2d67e..1781ce68 100644
--- a/app-static/template/htmltemplate.cpp
+++ b/app-static/template/htmltemplate.cpp
@@ -108,6 +108,7 @@ QString HtmlTemplate::buildHtmlHeader(RenderOptions options) const
// add mermaid.js script to HTML header
if (options.testFlag(Template::DiagramSupport)) {
+ header += "\n";
header += "\n";
}
diff --git a/app/resources.qrc b/app/resources.qrc
index 508dfc21..9f1c91bc 100644
--- a/app/resources.qrc
+++ b/app/resources.qrc
@@ -45,5 +45,6 @@
scripts/highlight.js/styles/github.css
scripts/highlight.js/styles/solarized_dark.css
scripts/mermaid/mermaid.full.min.js
+ scripts/mermaid/mermaid.css
diff --git a/app/scripts/mermaid/mermaid.css b/app/scripts/mermaid/mermaid.css
new file mode 100644
index 00000000..92802a46
--- /dev/null
+++ b/app/scripts/mermaid/mermaid.css
@@ -0,0 +1,256 @@
+/* Flowchart variables */
+/* Sequence Diagram variables */
+/* Gantt chart variables */
+.mermaid .label {
+ color: #333333;
+}
+.node rect,
+.node circle,
+.node polygon {
+ fill: #ececff;
+ stroke: #ccccff;
+ stroke-width: 1px;
+}
+.edgePath .path {
+ stroke: #333333;
+}
+.cluster rect {
+ fill: #ffffde;
+ rx: 40;
+ stroke: #aaaa33;
+ stroke-width: 1px;
+}
+.cluster text {
+ fill: #333333;
+}
+.actor {
+ stroke: #ccccff;
+ fill: #ececff;
+}
+text.actor {
+ fill: black;
+ stroke: none;
+}
+.actor-line {
+ stroke: grey;
+}
+.messageLine0 {
+ stroke-width: 1.5;
+ stroke-dasharray: "2 2";
+ marker-end: "url(#arrowhead)";
+ stroke: #333333;
+}
+.messageLine1 {
+ stroke-width: 1.5;
+ stroke-dasharray: "2 2";
+ stroke: #333333;
+}
+#arrowhead {
+ fill: #333333;
+}
+#crosshead path {
+ fill: #333333 !important;
+ stroke: #333333 !important;
+}
+.messageText {
+ fill: #333333;
+ stroke: none;
+}
+.labelBox {
+ stroke: #ccccff;
+ fill: #ececff;
+}
+.labelText {
+ fill: black;
+ stroke: none;
+}
+.loopText {
+ fill: black;
+ stroke: none;
+}
+.loopLine {
+ stroke-width: 2;
+ stroke-dasharray: "2 2";
+ marker-end: "url(#arrowhead)";
+ stroke: #ccccff;
+}
+.note {
+ stroke: #aaaa33;
+ fill: #fff5ad;
+}
+.noteText {
+ fill: black;
+ stroke: none;
+ font-family: 'trebuchet ms', verdana, arial;
+ font-size: 14px;
+}
+/** Section styling */
+.section {
+ stroke: none;
+ opacity: 0.2;
+}
+.section0 {
+ fill: rgba(102, 102, 255, 0.49);
+}
+.section2 {
+ fill: #fff400;
+}
+.section1,
+.section3 {
+ fill: white;
+ opacity: 0.2;
+}
+.sectionTitle0 {
+ fill: #333333;
+}
+.sectionTitle1 {
+ fill: #333333;
+}
+.sectionTitle2 {
+ fill: #333333;
+}
+.sectionTitle3 {
+ fill: #333333;
+}
+.sectionTitle {
+ text-anchor: start;
+ font-size: 11px;
+ text-height: 14px;
+}
+/* Grid and axis */
+.grid .tick {
+ stroke: lightgrey;
+ opacity: 0.3;
+ shape-rendering: crispEdges;
+}
+.grid path {
+ stroke-width: 0;
+}
+/* Today line */
+.today {
+ fill: none;
+ stroke: red;
+ stroke-width: 2px;
+}
+/* Task styling */
+/* Default task */
+.task {
+ stroke-width: 2;
+}
+.taskText {
+ text-anchor: middle;
+ font-size: 11px;
+}
+.taskTextOutsideRight {
+ fill: black;
+ text-anchor: start;
+ font-size: 11px;
+}
+.taskTextOutsideLeft {
+ fill: black;
+ text-anchor: end;
+ font-size: 11px;
+}
+/* Specific task settings for the sections*/
+.taskText0,
+.taskText1,
+.taskText2,
+.taskText3 {
+ fill: white;
+}
+.task0,
+.task1,
+.task2,
+.task3 {
+ fill: #8a90dd;
+ stroke: #534fbc;
+}
+.taskTextOutside0,
+.taskTextOutside2 {
+ fill: black;
+}
+.taskTextOutside1,
+.taskTextOutside3 {
+ fill: black;
+}
+/* Active task */
+.active0,
+.active1,
+.active2,
+.active3 {
+ fill: #bfc7ff;
+ stroke: #534fbc;
+}
+.activeText0,
+.activeText1,
+.activeText2,
+.activeText3 {
+ fill: black !important;
+}
+/* Completed task */
+.done0,
+.done1,
+.done2,
+.done3 {
+ stroke: grey;
+ fill: lightgrey;
+ stroke-width: 2;
+}
+.doneText0,
+.doneText1,
+.doneText2,
+.doneText3 {
+ fill: black !important;
+}
+/* Tasks on the critical line */
+.crit0,
+.crit1,
+.crit2,
+.crit3 {
+ stroke: #ff8888;
+ fill: red;
+ stroke-width: 2;
+}
+.activeCrit0,
+.activeCrit1,
+.activeCrit2,
+.activeCrit3 {
+ stroke: #ff8888;
+ fill: #bfc7ff;
+ stroke-width: 2;
+}
+.doneCrit0,
+.doneCrit1,
+.doneCrit2,
+.doneCrit3 {
+ stroke: #ff8888;
+ fill: lightgrey;
+ stroke-width: 2;
+ cursor: pointer;
+ shape-rendering: crispEdges;
+}
+.doneCritText0,
+.doneCritText1,
+.doneCritText2,
+.doneCritText3 {
+ fill: black !important;
+}
+.activeCritText0,
+.activeCritText1,
+.activeCritText2,
+.activeCritText3 {
+ fill: black !important;
+}
+.titleText {
+ text-anchor: middle;
+ font-size: 18px;
+ fill: black;
+}
+/*
+
+
+*/
+text {
+ font-family: 'trebuchet ms', verdana, arial;
+ font-size: 14px;
+}
From 90e21e2aecb49a549d0d262e58f950dda0d93d2f Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Wed, 28 Oct 2015 15:52:13 +0100
Subject: [PATCH 03/74] Reindent file
---
test/integration/htmltemplatetest.cpp | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/test/integration/htmltemplatetest.cpp b/test/integration/htmltemplatetest.cpp
index 59065be3..13e26b49 100644
--- a/test/integration/htmltemplatetest.cpp
+++ b/test/integration/htmltemplatetest.cpp
@@ -28,33 +28,33 @@ static const QString HIGHLIGHT_JS = QStringLiteral("TEST
", 0);
+ QString html = htmlTemplate.render("TEST
", 0);
const QString expected = QStringLiteral("%1\nTEST
")
- .arg(SCROLL_SCRIPT);
- QCOMPARE(html, expected);
+ .arg(SCROLL_SCRIPT);
+ QCOMPARE(html, expected);
}
void HtmlTemplateTest::rendersMermaidGraphInsideCodeTags()
{
- HtmlTemplate htmlTemplate(HTML_TEMPLATE);
+ HtmlTemplate htmlTemplate(HTML_TEMPLATE);
- QString html = htmlTemplate.render("TEST
", HtmlTemplate::DiagramSupport);
+ QString html = htmlTemplate.render("TEST
", HtmlTemplate::DiagramSupport);
const QString expected = QStringLiteral("%1\n%2\nTEST
")
- .arg(SCROLL_SCRIPT).arg(MERMAID_JS);
- QCOMPARE(html, expected);
+ .arg(SCROLL_SCRIPT).arg(MERMAID_JS);
+ QCOMPARE(html, expected);
}
void HtmlTemplateTest::replacesMermaidCodeTagsByDivTagsIfCodeHighlightingEnabled()
{
- HtmlTemplate htmlTemplate(HTML_TEMPLATE);
+ HtmlTemplate htmlTemplate(HTML_TEMPLATE);
- QString html = htmlTemplate.render("TEST
", HtmlTemplate::DiagramSupport | HtmlTemplate::CodeHighlighting);
+ QString html = htmlTemplate.render("TEST
", HtmlTemplate::DiagramSupport | HtmlTemplate::CodeHighlighting);
const QString expected = QStringLiteral("%1\n%2\n%3\n\nTEST
")
- .arg(SCROLL_SCRIPT).arg(HIGHLIGHT_JS).arg(MERMAID_JS);
- QCOMPARE(html, expected);
+ .arg(SCROLL_SCRIPT).arg(HIGHLIGHT_JS).arg(MERMAID_JS);
+ QCOMPARE(html, expected);
}
From 7dc7d7f6b00c25982be09bb00a8ef3ed66748ad1 Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Wed, 28 Oct 2015 18:35:29 +0100
Subject: [PATCH 04/74] Fix integration tests after changes to HtmlTemplate
class
---
test/integration/htmltemplatetest.cpp | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/test/integration/htmltemplatetest.cpp b/test/integration/htmltemplatetest.cpp
index 13e26b49..750d7941 100644
--- a/test/integration/htmltemplatetest.cpp
+++ b/test/integration/htmltemplatetest.cpp
@@ -23,6 +23,7 @@
static const QString HTML_TEMPLATE = QStringLiteral("");
static const QString SCROLL_SCRIPT = QStringLiteral("");
+static const QString MERMAID_CSS = QStringLiteral("");
static const QString MERMAID_JS = QStringLiteral("");
static const QString HIGHLIGHT_JS = QStringLiteral("\n\n");
@@ -43,8 +44,8 @@ void HtmlTemplateTest::rendersMermaidGraphInsideCodeTags()
QString html = htmlTemplate.render("TEST
", HtmlTemplate::DiagramSupport);
- const QString expected = QStringLiteral("%1\n%2\nTEST
")
- .arg(SCROLL_SCRIPT).arg(MERMAID_JS);
+ const QString expected = QStringLiteral("%1\n%2\n%3\nTEST
")
+ .arg(SCROLL_SCRIPT).arg(MERMAID_CSS).arg(MERMAID_JS);
QCOMPARE(html, expected);
}
@@ -54,7 +55,7 @@ void HtmlTemplateTest::replacesMermaidCodeTagsByDivTagsIfCodeHighlightingEnabled
QString html = htmlTemplate.render("TEST
", HtmlTemplate::DiagramSupport | HtmlTemplate::CodeHighlighting);
- const QString expected = QStringLiteral("%1\n%2\n%3\n\nTEST
")
- .arg(SCROLL_SCRIPT).arg(HIGHLIGHT_JS).arg(MERMAID_JS);
+ const QString expected = QStringLiteral("%1\n%2\n%3\n%4\n\nTEST
")
+ .arg(SCROLL_SCRIPT).arg(HIGHLIGHT_JS).arg(MERMAID_CSS).arg(MERMAID_JS);
QCOMPARE(html, expected);
}
From 7218d8b85fff8471bd1f93f5be1ecd47960973e6 Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Fri, 20 Nov 2015 19:46:48 +0100
Subject: [PATCH 05/74] [#232] Remember scrollbar position after
synchronization
Remember vertical scrollbar position of the editor after it
has been changed. This should prevent the jumping of the
editor pane during editing of the markdown text.
---
app-static/htmlviewsynchronizer.cpp | 3 +++
1 file changed, 3 insertions(+)
diff --git a/app-static/htmlviewsynchronizer.cpp b/app-static/htmlviewsynchronizer.cpp
index 87905e02..3ef6061c 100644
--- a/app-static/htmlviewsynchronizer.cpp
+++ b/app-static/htmlviewsynchronizer.cpp
@@ -46,6 +46,9 @@ void HtmlViewSynchronizer::webViewScrolled()
int value = m_webView->page()->mainFrame()->scrollBarValue(Qt::Vertical);
m_editor->verticalScrollBar()->setValue(qRound(value * factor));
+
+ // remember new vertical scrollbar position of markdown editor
+ rememberScrollBarPos();
}
void HtmlViewSynchronizer::scrollValueChanged(int value)
From 93673ad21551c635babd52da0d5e21974706204e Mon Sep 17 00:00:00 2001
From: Matthias Pohl
Date: Mon, 30 Nov 2015 11:05:43 +0100
Subject: [PATCH 06/74] Ensure that document is of correct type
---
app-static/converter/revealmarkdownconverter.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app-static/converter/revealmarkdownconverter.cpp b/app-static/converter/revealmarkdownconverter.cpp
index ff911468..f20bf2b7 100644
--- a/app-static/converter/revealmarkdownconverter.cpp
+++ b/app-static/converter/revealmarkdownconverter.cpp
@@ -30,7 +30,8 @@ QString RevealMarkdownConverter::renderAsHtml(MarkdownDocument *document)
if (document) {
RevealMarkdownDocument *doc = dynamic_cast(document);
- html = doc->markdownText;
+ if(doc)
+ html = doc->markdownText;
}
return html;
From 92ca3786161dc7b3fc7af2fbc3a6bee3fd968b27 Mon Sep 17 00:00:00 2001
From: Matthias Pohl
Date: Mon, 30 Nov 2015 13:11:56 +0100
Subject: [PATCH 07/74] fix for hoedown converter as well and code styling
---
app-static/converter/hoedownmarkdownconverter.cpp | 2 +-
app-static/converter/revealmarkdownconverter.cpp | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/app-static/converter/hoedownmarkdownconverter.cpp b/app-static/converter/hoedownmarkdownconverter.cpp
index 46392fe6..97808bee 100644
--- a/app-static/converter/hoedownmarkdownconverter.cpp
+++ b/app-static/converter/hoedownmarkdownconverter.cpp
@@ -88,7 +88,7 @@ QString HoedownMarkdownConverter::renderAsHtml(MarkdownDocument *document)
if (document) {
HoedownMarkdownDocument *doc = dynamic_cast(document);
- if (doc->document()) {
+ if (doc && doc->document()) {
hoedown_buffer *in = doc->document();
hoedown_buffer *out = hoedown_buffer_new(64);
diff --git a/app-static/converter/revealmarkdownconverter.cpp b/app-static/converter/revealmarkdownconverter.cpp
index f20bf2b7..72489a14 100644
--- a/app-static/converter/revealmarkdownconverter.cpp
+++ b/app-static/converter/revealmarkdownconverter.cpp
@@ -30,8 +30,9 @@ QString RevealMarkdownConverter::renderAsHtml(MarkdownDocument *document)
if (document) {
RevealMarkdownDocument *doc = dynamic_cast(document);
- if(doc)
+ if (doc) {
html = doc->markdownText;
+ }
}
return html;
From 2ac71ccc455df411471269d9a5ec1e52c11bb40a Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Wed, 9 Dec 2015 09:43:57 +0100
Subject: [PATCH 08/74] Introduce Theme class
A theme represents the combination of markdown, code and preview styles.
---
app-static/app-static.pro | 2 +
app-static/themes/theme.cpp | 45 ++++++++++++++++++++
app-static/themes/theme.h | 60 ++++++++++++++++++++++++++
test/unit/main.cpp | 6 ++-
test/unit/themetest.cpp | 84 +++++++++++++++++++++++++++++++++++++
test/unit/themetest.h | 36 ++++++++++++++++
test/unit/unit.pro | 6 ++-
7 files changed, 236 insertions(+), 3 deletions(-)
create mode 100644 app-static/themes/theme.cpp
create mode 100644 app-static/themes/theme.h
create mode 100644 test/unit/themetest.cpp
create mode 100644 test/unit/themetest.h
diff --git a/app-static/app-static.pro b/app-static/app-static.pro
index 1e760ff5..07ba25e2 100644
--- a/app-static/app-static.pro
+++ b/app-static/app-static.pro
@@ -21,6 +21,7 @@ SOURCES += \
converter/revealmarkdownconverter.cpp \
template/htmltemplate.cpp \
template/presentationtemplate.cpp \
+ themes/theme.cpp \
completionlistmodel.cpp \
datalocation.cpp \
slidelinemapping.cpp \
@@ -43,6 +44,7 @@ HEADERS += \
template/template.h \
template/htmltemplate.h \
template/presentationtemplate.h \
+ themes/theme.h \
completionlistmodel.h \
datalocation.h \
slidelinemapping.h \
diff --git a/app-static/themes/theme.cpp b/app-static/themes/theme.cpp
new file mode 100644
index 00000000..7a88d417
--- /dev/null
+++ b/app-static/themes/theme.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "theme.h"
+
+Theme::Theme(const QString &name,
+ const QString &markdownHighlighting,
+ const QString &codeHighlighting,
+ const QString &previewStylesheet) :
+ m_name(name),
+ m_markdownHighlighting(markdownHighlighting),
+ m_codeHighlighting(codeHighlighting),
+ m_previewStylesheet(previewStylesheet)
+{
+ checkInvariants();
+}
+
+void Theme::checkInvariants() const
+{
+ if (m_name.isEmpty()) {
+ throw std::runtime_error("theme name must not be empty");
+ }
+ if (m_markdownHighlighting.isEmpty()) {
+ throw std::runtime_error("markdown highlighting style must not be empty");
+ }
+ if (m_codeHighlighting.isEmpty()) {
+ throw std::runtime_error("code highlighting style must not be empty");
+ }
+ if (m_previewStylesheet.isEmpty()) {
+ throw std::runtime_error("preview stylesheet must not be empty");
+ }
+}
diff --git a/app-static/themes/theme.h b/app-static/themes/theme.h
new file mode 100644
index 00000000..94b4c5c2
--- /dev/null
+++ b/app-static/themes/theme.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 THEME_H
+#define THEME_H
+
+#include
+#include
+
+
+class Theme
+{
+public:
+ Theme(const QString &name,
+ const QString &markdownHighlighting,
+ const QString &codeHighlighting,
+ const QString &previewStylesheet);
+
+ QString name() const { return m_name; }
+
+ QString markdownHighlighting() const { return m_markdownHighlighting; }
+
+ QString codeHighlighting() const { return m_codeHighlighting; }
+
+ QString previewStylesheet() const { return m_previewStylesheet; }
+
+ bool operator<(const Theme &rhs) const
+ {
+ return m_name < rhs.name();
+ }
+
+ bool operator ==(const Theme &rhs) const
+ {
+ return m_name == rhs.name();
+ }
+
+private:
+ void checkInvariants() const;
+
+private:
+ QString m_name;
+ QString m_markdownHighlighting;
+ QString m_codeHighlighting;
+ QString m_previewStylesheet;
+};
+
+#endif // THEME_H
diff --git a/test/unit/main.cpp b/test/unit/main.cpp
index 155b2995..2b0c98ea 100644
--- a/test/unit/main.cpp
+++ b/test/unit/main.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2014 Christian Loose
+ * Copyright 2013-2015 Christian Loose
*
* 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
@@ -23,6 +23,7 @@
#include "snippetcollectiontest.h"
#include "completionlistmodeltest.h"
#include "snippettest.h"
+#include "themetest.h"
#include "yamlheadercheckertest.h"
int main(int argc, char *argv[])
@@ -53,5 +54,8 @@ int main(int argc, char *argv[])
YamlHeaderCheckerTest test8;
ret += QTest::qExec(&test8, argc, argv);
+ ThemeTest test9;
+ ret += QTest::qExec(&test9, argc, argv);
+
return ret;
}
diff --git a/test/unit/themetest.cpp b/test/unit/themetest.cpp
new file mode 100644
index 00000000..5d97e36b
--- /dev/null
+++ b/test/unit/themetest.cpp
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "themetest.h"
+
+#include
+
+#include
+
+static const QLatin1String A_THEME_NAME("name");
+static const QLatin1String A_MARKDOWN_HIGHLIGHTING("markdown");
+static const QLatin1String A_CODE_HIGHLIGHTING("code");
+static const QLatin1String A_PREVIEW_STYLESHEET("preview");
+
+void ThemeTest::isLessThanComparable()
+{
+ Theme theme1("abc", A_MARKDOWN_HIGHLIGHTING, A_CODE_HIGHLIGHTING, A_PREVIEW_STYLESHEET);
+
+ Theme theme2("xyz", A_MARKDOWN_HIGHLIGHTING, A_CODE_HIGHLIGHTING, A_PREVIEW_STYLESHEET);
+
+ QCOMPARE(theme1 < theme2, true);
+ QCOMPARE(theme2 < theme1, false);
+ QCOMPARE(theme1 < theme1, false);
+}
+
+void ThemeTest::isEqualComparable()
+{
+ Theme theme1("abc", A_MARKDOWN_HIGHLIGHTING, A_CODE_HIGHLIGHTING, A_PREVIEW_STYLESHEET);
+
+ Theme theme2("abc", A_MARKDOWN_HIGHLIGHTING, A_CODE_HIGHLIGHTING, A_PREVIEW_STYLESHEET);
+
+ Theme theme3("xyz", A_MARKDOWN_HIGHLIGHTING, A_CODE_HIGHLIGHTING, A_PREVIEW_STYLESHEET);
+
+ QCOMPARE(theme1 == theme1, true);
+ QCOMPARE(theme1 == theme2, true);
+ QCOMPARE(theme1 == theme3, false);
+}
+
+void ThemeTest::throwsIfNameIsEmpty()
+{
+ try {
+ Theme theme("", A_MARKDOWN_HIGHLIGHTING, A_CODE_HIGHLIGHTING, A_PREVIEW_STYLESHEET);
+ QFAIL("Expected exception of type runtime_error not thrown");
+ } catch(const std::runtime_error &) {}
+}
+
+void ThemeTest::throwsIfMarkdownHighlightingIsEmpty()
+{
+ try {
+ Theme theme(A_THEME_NAME, "", A_CODE_HIGHLIGHTING, A_PREVIEW_STYLESHEET);
+ QFAIL("Expected exception of type runtime_error not thrown");
+ } catch(const std::runtime_error &) {}
+}
+
+void ThemeTest::throwsIfCodeHighlightingIsEmpty()
+{
+ try {
+ Theme theme(A_THEME_NAME, A_MARKDOWN_HIGHLIGHTING, "", A_PREVIEW_STYLESHEET);
+ QFAIL("Expected exception of type runtime_error not thrown");
+ } catch(const std::runtime_error &) {}
+}
+
+void ThemeTest::throwsIfPreviewStylesheetIsEmpty()
+{
+ try {
+ Theme theme(A_THEME_NAME, A_MARKDOWN_HIGHLIGHTING, A_CODE_HIGHLIGHTING, "");
+ QFAIL("Expected exception of type runtime_error not thrown");
+ } catch(const std::runtime_error &) {}
+}
+
+
diff --git a/test/unit/themetest.h b/test/unit/themetest.h
new file mode 100644
index 00000000..9e567742
--- /dev/null
+++ b/test/unit/themetest.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 THEMETEST_H
+#define THEMETEST_H
+
+#include
+
+class ThemeTest : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void isLessThanComparable();
+ void isEqualComparable();
+ void throwsIfNameIsEmpty();
+ void throwsIfMarkdownHighlightingIsEmpty();
+ void throwsIfCodeHighlightingIsEmpty();
+ void throwsIfPreviewStylesheetIsEmpty();
+};
+
+#endif // THEMETEST_H
+
diff --git a/test/unit/unit.pro b/test/unit/unit.pro
index ac004fe8..4742605f 100644
--- a/test/unit/unit.pro
+++ b/test/unit/unit.pro
@@ -18,7 +18,8 @@ SOURCES += \
slidelinemappingtest.cpp \
snippetcollectiontest.cpp \
dictionarytest.cpp \
- yamlheadercheckertest.cpp
+ yamlheadercheckertest.cpp \
+ themetest.cpp
HEADERS += \
completionlistmodeltest.h \
@@ -28,7 +29,8 @@ HEADERS += \
slidelinemappingtest.h \
snippetcollectiontest.h \
dictionarytest.h \
- yamlheadercheckertest.h
+ yamlheadercheckertest.h \
+ themetest.h
target.CONFIG += no_default_install
From 8b227fd2db508336b1662db9fce438b9ec47bd1a Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Wed, 9 Dec 2015 11:42:22 +0100
Subject: [PATCH 09/74] Introduce ThemeCollection class
A collection of themes that can be loaded from a JSON file.
---
app-static/app-static.pro | 2 ++
app-static/themes/themecollection.cpp | 40 ++++++++++++++++++++++
app-static/themes/themecollection.h | 42 +++++++++++++++++++++++
test/unit/main.cpp | 4 +++
test/unit/themecollectiontest.cpp | 49 +++++++++++++++++++++++++++
test/unit/themecollectiontest.h | 34 +++++++++++++++++++
test/unit/unit.pro | 6 ++--
7 files changed, 175 insertions(+), 2 deletions(-)
create mode 100644 app-static/themes/themecollection.cpp
create mode 100644 app-static/themes/themecollection.h
create mode 100644 test/unit/themecollectiontest.cpp
create mode 100644 test/unit/themecollectiontest.h
diff --git a/app-static/app-static.pro b/app-static/app-static.pro
index 07ba25e2..5a2c9eac 100644
--- a/app-static/app-static.pro
+++ b/app-static/app-static.pro
@@ -22,6 +22,7 @@ SOURCES += \
template/htmltemplate.cpp \
template/presentationtemplate.cpp \
themes/theme.cpp \
+ themes/themecollection.cpp \
completionlistmodel.cpp \
datalocation.cpp \
slidelinemapping.cpp \
@@ -45,6 +46,7 @@ HEADERS += \
template/htmltemplate.h \
template/presentationtemplate.h \
themes/theme.h \
+ themes/themecollection.h \
completionlistmodel.h \
datalocation.h \
slidelinemapping.h \
diff --git a/app-static/themes/themecollection.cpp b/app-static/themes/themecollection.cpp
new file mode 100644
index 00000000..e9f86c1a
--- /dev/null
+++ b/app-static/themes/themecollection.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "themecollection.h"
+
+
+int ThemeCollection::insert(const Theme &theme)
+{
+ themesIndex << theme.name();
+ themes << theme;
+ return 0;
+}
+
+int ThemeCollection::count() const
+{
+ return themes.count();
+}
+
+const Theme &ThemeCollection::at(int offset) const
+{
+ return themes.at(offset);
+}
+
+const QString ThemeCollection::name() const
+{
+ return QStringLiteral("themes");
+}
diff --git a/app-static/themes/themecollection.h b/app-static/themes/themecollection.h
new file mode 100644
index 00000000..ca7a18e0
--- /dev/null
+++ b/app-static/themes/themecollection.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 THEMECOLLECTION_H
+#define THEMECOLLECTION_H
+
+#include
+#include
+#include
+#include "theme.h"
+
+
+class ThemeCollection : public JsonCollection
+{
+public:
+ int insert(const Theme &theme);
+
+ int count() const;
+ const Theme &at(int offset) const;
+
+ const QString name() const;
+
+private:
+ QStringList themesIndex;
+ QList themes;
+};
+
+#endif // THEMECOLLECTION_H
+
diff --git a/test/unit/main.cpp b/test/unit/main.cpp
index 2b0c98ea..9b247d73 100644
--- a/test/unit/main.cpp
+++ b/test/unit/main.cpp
@@ -23,6 +23,7 @@
#include "snippetcollectiontest.h"
#include "completionlistmodeltest.h"
#include "snippettest.h"
+#include "themecollectiontest.h"
#include "themetest.h"
#include "yamlheadercheckertest.h"
@@ -57,5 +58,8 @@ int main(int argc, char *argv[])
ThemeTest test9;
ret += QTest::qExec(&test9, argc, argv);
+ ThemeCollectionTest test10;
+ ret += QTest::qExec(&test10, argc, argv);
+
return ret;
}
diff --git a/test/unit/themecollectiontest.cpp b/test/unit/themecollectiontest.cpp
new file mode 100644
index 00000000..6a157495
--- /dev/null
+++ b/test/unit/themecollectiontest.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "themecollectiontest.h"
+
+#include
+
+#include
+
+
+void ThemeCollectionTest::returnsConstantNameOfJsonArray()
+{
+ ThemeCollection collection;
+ QCOMPARE(collection.name(), QStringLiteral("themes"));
+}
+
+void ThemeCollectionTest::returnsNumberOfThemesInCollection()
+{
+ ThemeCollection collection;
+ Theme theme("name", "markdown", "code", "preview");
+ collection.insert(theme);
+
+ QCOMPARE(collection.count(), 1);
+}
+
+void ThemeCollectionTest::returnsThemeAtIndexPosition()
+{
+ ThemeCollection collection;
+ Theme theme("name", "markdown", "code", "preview");
+ collection.insert(theme);
+
+ Theme actual = collection.at(0);
+
+ QCOMPARE(actual, theme);
+}
+
diff --git a/test/unit/themecollectiontest.h b/test/unit/themecollectiontest.h
new file mode 100644
index 00000000..df274fdd
--- /dev/null
+++ b/test/unit/themecollectiontest.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 THEMECOLLECTIONTEST_H
+#define THEMECOLLECTIONTEST_H
+
+#include
+
+class ThemeCollectionTest : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void returnsConstantNameOfJsonArray();
+ void returnsNumberOfThemesInCollection();
+ void returnsThemeAtIndexPosition();
+};
+
+#endif // THEMECOLLECTIONTEST_H
+
+
diff --git a/test/unit/unit.pro b/test/unit/unit.pro
index 4742605f..336f9284 100644
--- a/test/unit/unit.pro
+++ b/test/unit/unit.pro
@@ -19,7 +19,8 @@ SOURCES += \
snippetcollectiontest.cpp \
dictionarytest.cpp \
yamlheadercheckertest.cpp \
- themetest.cpp
+ themetest.cpp \
+ themecollectiontest.cpp
HEADERS += \
completionlistmodeltest.h \
@@ -30,7 +31,8 @@ HEADERS += \
snippetcollectiontest.h \
dictionarytest.h \
yamlheadercheckertest.h \
- themetest.h
+ themetest.h \
+ themecollectiontest.h
target.CONFIG += no_default_install
From cd8ea26f63d9a2c32570c12a59eb8ce3ff91ca1a Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Wed, 9 Dec 2015 12:47:21 +0100
Subject: [PATCH 10/74] Add JSON translator for themes
Add JsonThemeTranslator class to translate between JSON and Theme
objects.
---
app-static/app-static.pro | 2 +
app-static/themes/jsonthemetranslator.cpp | 48 +++++++++
app-static/themes/jsonthemetranslator.h | 34 +++++++
test/unit/jsonthemetranslatortest.cpp | 117 ++++++++++++++++++++++
test/unit/jsonthemetranslatortest.h | 44 ++++++++
test/unit/main.cpp | 4 +
test/unit/unit.pro | 2 +
7 files changed, 251 insertions(+)
create mode 100644 app-static/themes/jsonthemetranslator.cpp
create mode 100644 app-static/themes/jsonthemetranslator.h
create mode 100644 test/unit/jsonthemetranslatortest.cpp
create mode 100644 test/unit/jsonthemetranslatortest.h
diff --git a/app-static/app-static.pro b/app-static/app-static.pro
index 5a2c9eac..ec4f8a97 100644
--- a/app-static/app-static.pro
+++ b/app-static/app-static.pro
@@ -21,6 +21,7 @@ SOURCES += \
converter/revealmarkdownconverter.cpp \
template/htmltemplate.cpp \
template/presentationtemplate.cpp \
+ themes/jsonthemetranslator.cpp \
themes/theme.cpp \
themes/themecollection.cpp \
completionlistmodel.cpp \
@@ -45,6 +46,7 @@ HEADERS += \
template/template.h \
template/htmltemplate.h \
template/presentationtemplate.h \
+ themes/jsonthemetranslator.h \
themes/theme.h \
themes/themecollection.h \
completionlistmodel.h \
diff --git a/app-static/themes/jsonthemetranslator.cpp b/app-static/themes/jsonthemetranslator.cpp
new file mode 100644
index 00000000..fb671b7d
--- /dev/null
+++ b/app-static/themes/jsonthemetranslator.cpp
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "jsonthemetranslator.h"
+
+namespace {
+
+static const QLatin1String NAME("name");
+static const QLatin1String MARKDOWN_HIGHLIGHTING("markdownHighlighting");
+static const QLatin1String CODE_HIGHLIGHTING("codeHighlighting");
+static const QLatin1String PREVIEW_STYLESHEET("previewStylesheet");
+
+}
+
+Theme JsonThemeTranslator::fromJsonObject(const QJsonObject &object)
+{
+ QString name = object.value(NAME).toString();
+ QString markdownHighlighting = object.value(MARKDOWN_HIGHLIGHTING).toString();
+ QString codeHighlighting = object.value(CODE_HIGHLIGHTING).toString();
+ QString previewStylesheet = object.value(PREVIEW_STYLESHEET).toString();
+
+ return { name, markdownHighlighting, codeHighlighting, previewStylesheet };
+}
+
+QJsonObject JsonThemeTranslator::toJsonObject(const Theme &theme)
+{
+ QJsonObject object;
+ object.insert(NAME, theme.name());
+ object.insert(MARKDOWN_HIGHLIGHTING, theme.markdownHighlighting());
+ object.insert(CODE_HIGHLIGHTING, theme.codeHighlighting());
+ object.insert(PREVIEW_STYLESHEET, theme.previewStylesheet());
+
+ return object;
+}
+
diff --git a/app-static/themes/jsonthemetranslator.h b/app-static/themes/jsonthemetranslator.h
new file mode 100644
index 00000000..de13f0d8
--- /dev/null
+++ b/app-static/themes/jsonthemetranslator.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 JSONTHEMETRANSLATOR_H
+#define JSONTHEMETRANSLATOR_H
+
+#include
+#include
+#include "theme.h"
+
+
+class JsonThemeTranslator : public JsonTranslator
+{
+private:
+ Theme fromJsonObject(const QJsonObject &object);
+ QJsonObject toJsonObject(const Theme &theme);
+};
+
+#endif // JSONTHEMETRANSLATOR_H
+
+
diff --git a/test/unit/jsonthemetranslatortest.cpp b/test/unit/jsonthemetranslatortest.cpp
new file mode 100644
index 00000000..8731f018
--- /dev/null
+++ b/test/unit/jsonthemetranslatortest.cpp
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "jsonthemetranslatortest.h"
+
+#include
+
+#include
+#include
+#include
+
+static const QLatin1String A_THEME_NAME("mytheme");
+static const QLatin1String A_MARKDOWN_HIGHLIGHTING("default");
+static const QLatin1String A_CODE_HIGHLIGHTING("monokai");
+static const QLatin1String A_PREVIEW_STYLESHEET("github");
+
+
+QJsonDocument NewJsonDocumentWithObject(const QJsonObject &jsonObject)
+{
+ QJsonArray themeArray;
+ themeArray.append(jsonObject);
+
+ QJsonObject object;
+ object.insert("themes", themeArray);
+
+ return QJsonDocument(object);
+}
+
+QJsonObject NewJsonThemeObject()
+{
+ QJsonObject jsonObject;
+ jsonObject.insert("name", A_THEME_NAME);
+ jsonObject.insert("markdownHighlighting", A_MARKDOWN_HIGHLIGHTING);
+ jsonObject.insert("codeHighlighting", A_CODE_HIGHLIGHTING);
+ jsonObject.insert("previewStylesheet", A_PREVIEW_STYLESHEET);
+
+ return jsonObject;
+}
+
+void JsonThemeTranslatorTest::initTestCase()
+{
+ translator = new JsonThemeTranslator();
+}
+
+void JsonThemeTranslatorTest::cleanupTestCase()
+{
+ delete translator;
+}
+
+void JsonThemeTranslatorTest::doesNotProcessInvalidJsonDocument()
+{
+ QJsonDocument doc;
+
+ ThemeCollection collection;
+ bool success = translator->processDocument(doc, &collection);
+
+ QVERIFY(!success);
+ QCOMPARE(collection.count(), 0);
+}
+
+void JsonThemeTranslatorTest::translatesEmptyJsonDocumentToEmptyThemes()
+{
+ QJsonObject themesObject;
+ themesObject.insert("themes", QJsonArray());
+
+ QJsonDocument doc(themesObject);
+
+ ThemeCollection collection;
+ bool success = translator->processDocument(doc, &collection);
+
+ QVERIFY(success);
+ QCOMPARE(collection.count(), 0);
+}
+
+void JsonThemeTranslatorTest::translatesJsonDocumentToThemes()
+{
+ QJsonDocument doc = NewJsonDocumentWithObject(NewJsonThemeObject());
+
+ ThemeCollection collection;
+ bool success = translator->processDocument(doc, &collection);
+
+ QVERIFY(success);
+ QCOMPARE(collection.count(), 1);
+ QCOMPARE(collection.at(0).name(), A_THEME_NAME);
+ QCOMPARE(collection.at(0).markdownHighlighting(), A_MARKDOWN_HIGHLIGHTING);
+ QCOMPARE(collection.at(0).codeHighlighting(), A_CODE_HIGHLIGHTING);
+ QCOMPARE(collection.at(0).previewStylesheet(), A_PREVIEW_STYLESHEET);
+}
+
+void JsonThemeTranslatorTest::translatesThemesToJsonDocument()
+{
+ Theme theme(A_THEME_NAME, A_MARKDOWN_HIGHLIGHTING, A_CODE_HIGHLIGHTING, A_PREVIEW_STYLESHEET);
+ ThemeCollection collection;
+ collection.insert(theme);
+
+ QJsonDocument doc = translator->createDocument(&collection);
+
+ QJsonObject actual = doc.object().value("themes").toArray().first().toObject();
+ QCOMPARE(actual["name"].toString(), A_THEME_NAME);
+ QCOMPARE(actual["markdownHighlighting"].toString(), A_MARKDOWN_HIGHLIGHTING);
+ QCOMPARE(actual["codeHighlighting"].toString(), A_CODE_HIGHLIGHTING);
+ QCOMPARE(actual["previewStylesheet"].toString(), A_PREVIEW_STYLESHEET);
+}
+
diff --git a/test/unit/jsonthemetranslatortest.h b/test/unit/jsonthemetranslatortest.h
new file mode 100644
index 00000000..ab44d4de
--- /dev/null
+++ b/test/unit/jsonthemetranslatortest.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 JSONTHEMETRANSLATORTEST_H
+#define JSONTHEMETRANSLATORTEST_H
+
+#include
+class JsonThemeTranslator;
+
+
+class JsonThemeTranslatorTest : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void doesNotProcessInvalidJsonDocument();
+ void translatesEmptyJsonDocumentToEmptyThemes();
+ void translatesJsonDocumentToThemes();
+ void translatesThemesToJsonDocument();
+
+private:
+ JsonThemeTranslator *translator;
+};
+
+#endif // JSONTHEMETRANSLATORTEST_H
+
+
+
diff --git a/test/unit/main.cpp b/test/unit/main.cpp
index 9b247d73..c271ee57 100644
--- a/test/unit/main.cpp
+++ b/test/unit/main.cpp
@@ -18,6 +18,7 @@
#include "dictionarytest.h"
#include "jsonsnippettranslatortest.h"
+#include "jsonthemetranslatortest.h"
#include "jsontranslatorfactorytest.h"
#include "slidelinemappingtest.h"
#include "snippetcollectiontest.h"
@@ -61,5 +62,8 @@ int main(int argc, char *argv[])
ThemeCollectionTest test10;
ret += QTest::qExec(&test10, argc, argv);
+ JsonThemeTranslatorTest test11;
+ ret += QTest::qExec(&test11, argc, argv);
+
return ret;
}
diff --git a/test/unit/unit.pro b/test/unit/unit.pro
index 336f9284..ca584408 100644
--- a/test/unit/unit.pro
+++ b/test/unit/unit.pro
@@ -14,6 +14,7 @@ SOURCES += \
completionlistmodeltest.cpp \
snippettest.cpp \
jsonsnippettranslatortest.cpp \
+ jsonthemetranslatortest.cpp \
jsontranslatorfactorytest.cpp \
slidelinemappingtest.cpp \
snippetcollectiontest.cpp \
@@ -26,6 +27,7 @@ HEADERS += \
completionlistmodeltest.h \
snippettest.h \
jsonsnippettranslatortest.h \
+ jsonthemetranslatortest.h \
jsontranslatorfactorytest.h \
slidelinemappingtest.h \
snippetcollectiontest.h \
From b7c73d071a9b5316be24901a335f6d0b0a0bd9a9 Mon Sep 17 00:00:00 2001
From: Matthias Pohl
Date: Mon, 30 Nov 2015 11:05:43 +0100
Subject: [PATCH 11/74] Ensure that document is of correct type
---
app-static/converter/revealmarkdownconverter.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app-static/converter/revealmarkdownconverter.cpp b/app-static/converter/revealmarkdownconverter.cpp
index ff911468..f20bf2b7 100644
--- a/app-static/converter/revealmarkdownconverter.cpp
+++ b/app-static/converter/revealmarkdownconverter.cpp
@@ -30,7 +30,8 @@ QString RevealMarkdownConverter::renderAsHtml(MarkdownDocument *document)
if (document) {
RevealMarkdownDocument *doc = dynamic_cast(document);
- html = doc->markdownText;
+ if(doc)
+ html = doc->markdownText;
}
return html;
From 52fdfb8d06342902cb89663e419b382975bf4d93 Mon Sep 17 00:00:00 2001
From: Matthias Pohl
Date: Mon, 30 Nov 2015 13:11:56 +0100
Subject: [PATCH 12/74] fix for hoedown converter as well and code styling
---
app-static/converter/hoedownmarkdownconverter.cpp | 2 +-
app-static/converter/revealmarkdownconverter.cpp | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/app-static/converter/hoedownmarkdownconverter.cpp b/app-static/converter/hoedownmarkdownconverter.cpp
index 46392fe6..97808bee 100644
--- a/app-static/converter/hoedownmarkdownconverter.cpp
+++ b/app-static/converter/hoedownmarkdownconverter.cpp
@@ -88,7 +88,7 @@ QString HoedownMarkdownConverter::renderAsHtml(MarkdownDocument *document)
if (document) {
HoedownMarkdownDocument *doc = dynamic_cast(document);
- if (doc->document()) {
+ if (doc && doc->document()) {
hoedown_buffer *in = doc->document();
hoedown_buffer *out = hoedown_buffer_new(64);
diff --git a/app-static/converter/revealmarkdownconverter.cpp b/app-static/converter/revealmarkdownconverter.cpp
index f20bf2b7..72489a14 100644
--- a/app-static/converter/revealmarkdownconverter.cpp
+++ b/app-static/converter/revealmarkdownconverter.cpp
@@ -30,8 +30,9 @@ QString RevealMarkdownConverter::renderAsHtml(MarkdownDocument *document)
if (document) {
RevealMarkdownDocument *doc = dynamic_cast(document);
- if(doc)
+ if (doc) {
html = doc->markdownText;
+ }
}
return html;
From aa2e2da925239711823d98db8c6f7891cfa83c77 Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Wed, 9 Dec 2015 15:15:08 +0100
Subject: [PATCH 13/74] Add integration test for reading themes from JSON file
---
app-static/app-static.pro | 1 +
.../themes/jsonthemetranslatorfactory.h | 37 +++++
test/integration/integration.pro | 2 +
test/integration/jsonthemefiletest.cpp | 153 ++++++++++++++++++
test/integration/jsonthemefiletest.h | 36 +++++
test/integration/main.cpp | 8 +-
6 files changed, 235 insertions(+), 2 deletions(-)
create mode 100644 app-static/themes/jsonthemetranslatorfactory.h
create mode 100644 test/integration/jsonthemefiletest.cpp
create mode 100644 test/integration/jsonthemefiletest.h
diff --git a/app-static/app-static.pro b/app-static/app-static.pro
index ec4f8a97..4fd70850 100644
--- a/app-static/app-static.pro
+++ b/app-static/app-static.pro
@@ -47,6 +47,7 @@ HEADERS += \
template/htmltemplate.h \
template/presentationtemplate.h \
themes/jsonthemetranslator.h \
+ themes/jsonthemetranslatorfactory.h \
themes/theme.h \
themes/themecollection.h \
completionlistmodel.h \
diff --git a/app-static/themes/jsonthemetranslatorfactory.h b/app-static/themes/jsonthemetranslatorfactory.h
new file mode 100644
index 00000000..5d4994b7
--- /dev/null
+++ b/app-static/themes/jsonthemetranslatorfactory.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 JSONTHEMETRANSLATORFACTORY_H
+#define JSONTHEMETRANSLATORFACTORY_H
+
+#include
+#include
+
+#include "themes/theme.h"
+#include "themes/jsonthemetranslator.h"
+
+
+template <> class JsonTranslatorFactory
+{
+public:
+ static JsonTranslator *create()
+ {
+ return new JsonThemeTranslator();
+ }
+};
+
+#endif // JSONTHEMETRANSLATORFACTORY_H
+
diff --git a/test/integration/integration.pro b/test/integration/integration.pro
index 786f4aeb..7936f8ba 100644
--- a/test/integration/integration.pro
+++ b/test/integration/integration.pro
@@ -15,6 +15,7 @@ SOURCES += \
htmlpreviewcontrollertest.cpp \
htmltemplatetest.cpp \
jsonsnippetfiletest.cpp \
+ jsonthemefiletest.cpp \
main.cpp \
pmhmarkdownparsertest.cpp \
revealmarkdownconvertertest.cpp
@@ -24,6 +25,7 @@ HEADERS += \
htmlpreviewcontrollertest.h \
htmltemplatetest.h \
jsonsnippetfiletest.h \
+ jsonthemefiletest.h \
pmhmarkdownparsertest.h \
revealmarkdownconvertertest.h
diff --git a/test/integration/jsonthemefiletest.cpp b/test/integration/jsonthemefiletest.cpp
new file mode 100644
index 00000000..effe6888
--- /dev/null
+++ b/test/integration/jsonthemefiletest.cpp
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "jsonthemefiletest.h"
+
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+
+void JsonThemeFileTest::loadsEmptyThemeCollectionFromFile()
+{
+ QTemporaryFile themeFile(this);
+ if (!themeFile.open())
+ QFAIL("Failed to create temporary theme file");
+
+ QTextStream out(&themeFile); out << "{ \"themes\": [] }";
+
+ themeFile.close();
+
+ ThemeCollection collection;
+ bool success = JsonFile::load(themeFile.fileName(), &collection);
+
+ QVERIFY(success);
+ QCOMPARE(collection.count(), 0);
+}
+
+void JsonThemeFileTest::loadsThemesCollectionFromFile()
+{
+ QTemporaryFile themeFile(this);
+ if (!themeFile.open())
+ QFAIL("Failed to create temporary theme file");
+
+ QTextStream out(&themeFile);
+ out << "{ \"themes\": ["
+ << " { \"name\": \"default\","
+ << " \"markdownHighlighting\": \"default\","
+ << " \"codeHighlighting\": \"default\","
+ << " \"previewStylesheet\": \"default\" },"
+ << " { \"name\": \"dark\","
+ << " \"markdownHighlighting\": \"dark\","
+ << " \"codeHighlighting\": \"black\","
+ << " \"previewStylesheet\": \"dark\" } ] }";
+
+ themeFile.close();
+
+ ThemeCollection collection;
+ bool success = JsonFile::load(themeFile.fileName(), &collection);
+
+ QVERIFY(success);
+ QCOMPARE(collection.count(), 2);
+ QCOMPARE(collection.at(0).name(), QLatin1String("default"));
+ QCOMPARE(collection.at(1).name(), QLatin1String("dark"));
+}
+
+void JsonThemeFileTest::savesEmptyThemesCollectionToFile()
+{
+ QTemporaryFile themeFile(this);
+ if (!themeFile.open())
+ QFAIL("Failed to create temporary theme file");
+
+ ThemeCollection collection;
+ bool success = JsonFile::save(themeFile.fileName(), &collection);
+
+ QVERIFY(success);
+
+ QTextStream in(&themeFile);
+ QString fileContent = in.readAll().trimmed();
+
+ QVERIFY(fileContent.startsWith("{"));
+ QVERIFY(fileContent.contains("\"themes\": ["));
+ QVERIFY(fileContent.endsWith("}"));
+}
+
+void JsonThemeFileTest::savesThemesCollectionToFile()
+{
+ QTemporaryFile themeFile(this);
+ if (!themeFile.open())
+ QFAIL("Failed to create temporary theme file");
+
+ Theme theme1("default", "default", "default", "default");
+
+ Theme theme2("dark", "dark", "black", "dark");
+
+ ThemeCollection collection;
+ collection.insert(theme1);
+ collection.insert(theme2);
+
+ bool success = JsonFile::save(themeFile.fileName(), &collection);
+
+ QVERIFY(success);
+
+ QTextStream in(&themeFile);
+ QString fileContent = in.readAll().trimmed();
+
+ QVERIFY(fileContent.startsWith("{"));
+ QVERIFY(fileContent.contains("\"themes\": ["));
+ QVERIFY(fileContent.contains("\"name\": \"default\""));
+ QVERIFY(fileContent.contains("\"name\": \"dark\""));
+ QVERIFY(fileContent.endsWith("}"));
+}
+
+void JsonThemeFileTest::roundtripTest()
+{
+ QTemporaryFile themeFile(this);
+ if (!themeFile.open())
+ QFAIL("Failed to create temporary theme file");
+
+ Theme theme1("default", "default", "default", "default");
+
+ Theme theme2("dark", "dark", "black", "dark");
+
+ ThemeCollection collection1;
+ collection1.insert(theme1);
+ collection1.insert(theme2);
+
+ bool saveSuccess = JsonFile::save(themeFile.fileName(), &collection1);
+ QVERIFY(saveSuccess);
+
+ ThemeCollection collection2;
+ bool loadSuccess = JsonFile::load(themeFile.fileName(), &collection2);
+ QVERIFY(loadSuccess);
+
+ QCOMPARE(collection2.count(), 2);
+
+ QCOMPARE(collection2.at(0).name(), theme1.name());
+ QCOMPARE(collection2.at(0).markdownHighlighting(), theme1.markdownHighlighting());
+ QCOMPARE(collection2.at(0).codeHighlighting(), theme1.codeHighlighting());
+ QCOMPARE(collection2.at(0).previewStylesheet(), theme1.previewStylesheet());
+
+ QCOMPARE(collection2.at(1).name(), theme2.name());
+ QCOMPARE(collection2.at(1).markdownHighlighting(), theme2.markdownHighlighting());
+ QCOMPARE(collection2.at(1).codeHighlighting(), theme2.codeHighlighting());
+ QCOMPARE(collection2.at(1).previewStylesheet(), theme2.previewStylesheet());
+}
diff --git a/test/integration/jsonthemefiletest.h b/test/integration/jsonthemefiletest.h
new file mode 100644
index 00000000..f2898303
--- /dev/null
+++ b/test/integration/jsonthemefiletest.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 JSONTHEMEFILETEST_H
+#define JSONTHEMEFILETEST_H
+
+#include
+
+class JsonThemeFileTest : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void loadsEmptyThemeCollectionFromFile();
+ void loadsThemesCollectionFromFile();
+
+ void savesEmptyThemesCollectionToFile();
+ void savesThemesCollectionToFile();
+
+ void roundtripTest();
+};
+
+#endif // JSONTHEMEFILETEST_H
diff --git a/test/integration/main.cpp b/test/integration/main.cpp
index 48f5d183..477668f4 100644
--- a/test/integration/main.cpp
+++ b/test/integration/main.cpp
@@ -22,6 +22,7 @@
#include "htmlpreviewcontrollertest.h"
#include "htmltemplatetest.h"
#include "jsonsnippetfiletest.h"
+#include "jsonthemefiletest.h"
#include "pmhmarkdownparsertest.h"
#include "revealmarkdownconvertertest.h"
@@ -55,8 +56,11 @@ int main(int argc, char *argv[])
HtmlPreviewControllerTest test6;
ret += QTest::qExec(&test6, argc, argv);
- HtmlTemplateTest test7;
- ret += QTest::qExec(&test7, argc, argv);
+ HtmlTemplateTest test7;
+ ret += QTest::qExec(&test7, argc, argv);
+
+ JsonThemeFileTest test8;
+ ret += QTest::qExec(&test8, argc, argv);
return ret;
}
From 7eb3e01cb386e941670708ca60976892e79e55d4 Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Wed, 9 Dec 2015 19:40:00 +0100
Subject: [PATCH 14/74] Add contains() method to ThemeCollection class
---
app-static/themes/themecollection.cpp | 5 +++++
app-static/themes/themecollection.h | 1 +
test/unit/themecollectiontest.cpp | 9 +++++++++
test/unit/themecollectiontest.h | 1 +
4 files changed, 16 insertions(+)
diff --git a/app-static/themes/themecollection.cpp b/app-static/themes/themecollection.cpp
index e9f86c1a..dc39cdb5 100644
--- a/app-static/themes/themecollection.cpp
+++ b/app-static/themes/themecollection.cpp
@@ -34,6 +34,11 @@ const Theme &ThemeCollection::at(int offset) const
return themes.at(offset);
}
+bool ThemeCollection::contains(const QString &name) const
+{
+ return themesIndex.contains(name);
+}
+
const QString ThemeCollection::name() const
{
return QStringLiteral("themes");
diff --git a/app-static/themes/themecollection.h b/app-static/themes/themecollection.h
index ca7a18e0..82191d96 100644
--- a/app-static/themes/themecollection.h
+++ b/app-static/themes/themecollection.h
@@ -30,6 +30,7 @@ class ThemeCollection : public JsonCollection
int count() const;
const Theme &at(int offset) const;
+ bool contains(const QString &name) const;
const QString name() const;
diff --git a/test/unit/themecollectiontest.cpp b/test/unit/themecollectiontest.cpp
index 6a157495..de7333df 100644
--- a/test/unit/themecollectiontest.cpp
+++ b/test/unit/themecollectiontest.cpp
@@ -47,3 +47,12 @@ void ThemeCollectionTest::returnsThemeAtIndexPosition()
QCOMPARE(actual, theme);
}
+void ThemeCollectionTest::returnsIfCollectionContainsTheme()
+{
+ ThemeCollection collection;
+ Theme theme("name", "markdown", "code", "preview");
+ collection.insert(theme);
+
+ QCOMPARE(collection.contains("name"), true);
+ QCOMPARE(collection.contains("missing"), false);
+}
diff --git a/test/unit/themecollectiontest.h b/test/unit/themecollectiontest.h
index df274fdd..28128ed6 100644
--- a/test/unit/themecollectiontest.h
+++ b/test/unit/themecollectiontest.h
@@ -27,6 +27,7 @@ private slots:
void returnsConstantNameOfJsonArray();
void returnsNumberOfThemesInCollection();
void returnsThemeAtIndexPosition();
+ void returnsIfCollectionContainsTheme();
};
#endif // THEMECOLLECTIONTEST_H
From 65f55c76d2c5aaa18ccc7793bd9af7a46fcb9699 Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Thu, 10 Dec 2015 19:38:32 +0100
Subject: [PATCH 15/74] Add theme() and themeNames() to ThemeCollection class
Add methods to retrieve a theme by name or the names of all themes to
the ThemeCollection class.
---
app-static/themes/themecollection.cpp | 10 ++++++++++
app-static/themes/themecollection.h | 2 ++
test/unit/themecollectiontest.cpp | 27 +++++++++++++++++++++++++++
test/unit/themecollectiontest.h | 2 ++
4 files changed, 41 insertions(+)
diff --git a/app-static/themes/themecollection.cpp b/app-static/themes/themecollection.cpp
index dc39cdb5..981767d8 100644
--- a/app-static/themes/themecollection.cpp
+++ b/app-static/themes/themecollection.cpp
@@ -39,6 +39,16 @@ bool ThemeCollection::contains(const QString &name) const
return themesIndex.contains(name);
}
+const Theme ThemeCollection::theme(const QString &name) const
+{
+ return at(themesIndex.indexOf(name));
+}
+
+QStringList ThemeCollection::themeNames() const
+{
+ return themesIndex;
+}
+
const QString ThemeCollection::name() const
{
return QStringLiteral("themes");
diff --git a/app-static/themes/themecollection.h b/app-static/themes/themecollection.h
index 82191d96..6c2c6ae5 100644
--- a/app-static/themes/themecollection.h
+++ b/app-static/themes/themecollection.h
@@ -31,6 +31,8 @@ class ThemeCollection : public JsonCollection
int count() const;
const Theme &at(int offset) const;
bool contains(const QString &name) const;
+ const Theme theme(const QString &name) const;
+ QStringList themeNames() const;
const QString name() const;
diff --git a/test/unit/themecollectiontest.cpp b/test/unit/themecollectiontest.cpp
index de7333df..3fbf9511 100644
--- a/test/unit/themecollectiontest.cpp
+++ b/test/unit/themecollectiontest.cpp
@@ -56,3 +56,30 @@ void ThemeCollectionTest::returnsIfCollectionContainsTheme()
QCOMPARE(collection.contains("name"), true);
QCOMPARE(collection.contains("missing"), false);
}
+
+void ThemeCollectionTest::returnsThemeByName()
+{
+ ThemeCollection collection;
+ Theme theme("name", "markdown", "code", "preview");
+ collection.insert(theme);
+
+ Theme actual = collection.theme("name");
+
+ QCOMPARE(actual, theme);
+}
+
+void ThemeCollectionTest::returnsNameOfAllThemes()
+{
+ Theme theme1("name 1", "markdown", "code", "preview");
+ Theme theme2("name 2", "markdown", "code", "preview");
+ ThemeCollection collection;
+ collection.insert(theme1);
+ collection.insert(theme2);
+
+ QStringList themeNames = collection.themeNames();
+
+ QCOMPARE(themeNames.count(), 2);
+ QCOMPARE(themeNames.at(0), theme1.name());
+ QCOMPARE(themeNames.at(1), theme2.name());
+}
+
diff --git a/test/unit/themecollectiontest.h b/test/unit/themecollectiontest.h
index 28128ed6..3fc8d15b 100644
--- a/test/unit/themecollectiontest.h
+++ b/test/unit/themecollectiontest.h
@@ -28,6 +28,8 @@ private slots:
void returnsNumberOfThemesInCollection();
void returnsThemeAtIndexPosition();
void returnsIfCollectionContainsTheme();
+ void returnsThemeByName();
+ void returnsNameOfAllThemes();
};
#endif // THEMECOLLECTIONTEST_H
From b37ed38574624517a384edb4de193e082c4aa90c Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Thu, 10 Dec 2015 19:48:16 +0100
Subject: [PATCH 16/74] Add JSON file with builtin HTML preview themes
---
app/builtin-htmlpreview-themes.json | 46 +++++++++++++++++++++++++++++
app/resources.qrc | 1 +
2 files changed, 47 insertions(+)
create mode 100644 app/builtin-htmlpreview-themes.json
diff --git a/app/builtin-htmlpreview-themes.json b/app/builtin-htmlpreview-themes.json
new file mode 100644
index 00000000..20beadda
--- /dev/null
+++ b/app/builtin-htmlpreview-themes.json
@@ -0,0 +1,46 @@
+{
+ "themes": [
+ {
+ "name": "Default",
+ "markdownHighlighting": "Default",
+ "codeHighlighting": "Default",
+ "previewStylesheet": "Default"
+ },
+ {
+ "name": "Github",
+ "markdownHighlighting": "Default",
+ "codeHighlighting": "Github",
+ "previewStylesheet": "Github"
+ },
+ {
+ "name": "Solarized Light",
+ "markdownHighlighting": "Solarized Light",
+ "codeHighlighting": "Solarized Light",
+ "previewStylesheet": "Solarized Light"
+ },
+ {
+ "name": "Solarized Dark",
+ "markdownHighlighting": "Solarized Dark",
+ "codeHighlighting": "Solarized Dark",
+ "previewStylesheet": "Solarized Dark"
+ },
+ {
+ "name": "Clearness",
+ "markdownHighlighting": "Default",
+ "codeHighlighting": "Default",
+ "previewStylesheet": "Clearness"
+ },
+ {
+ "name": "Clearness Dark",
+ "markdownHighlighting": "Clearness Dark",
+ "codeHighlighting": "Default",
+ "previewStylesheet": "Clearness Dark"
+ },
+ {
+ "name": "Byword Dark",
+ "markdownHighlighting": "Byword Dark",
+ "codeHighlighting": "Default",
+ "previewStylesheet": "Byword Dark"
+ }
+ ]
+}
diff --git a/app/resources.qrc b/app/resources.qrc
index c364e28d..b492a939 100644
--- a/app/resources.qrc
+++ b/app/resources.qrc
@@ -34,6 +34,7 @@
syntax_id.html
syntax_da.html
template_presentation.html
+ builtin-htmlpreview-themes.json
images/icon-close.png
From 58797e5bf4bac4bb980d097f299cf5edfc4a3926 Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Thu, 10 Dec 2015 19:50:20 +0100
Subject: [PATCH 17/74] Introduce ThemeManager class
It manages a theme collection and the association to the builtin
markdown, code and preview styles
---
app-static/app-static.pro | 2 +
app-static/themes/thememanager.cpp | 84 +++++++++++++++++++++++++++
app-static/themes/thememanager.h | 44 ++++++++++++++
test/integration/integration.pro | 6 +-
test/integration/main.cpp | 4 ++
test/integration/thememanagertest.cpp | 68 ++++++++++++++++++++++
test/integration/thememanagertest.h | 41 +++++++++++++
test/unit/main.cpp | 6 +-
test/unit/thememanagertest.cpp | 64 ++++++++++++++++++++
test/unit/thememanagertest.h | 34 +++++++++++
test/unit/unit.pro | 6 +-
11 files changed, 354 insertions(+), 5 deletions(-)
create mode 100644 app-static/themes/thememanager.cpp
create mode 100644 app-static/themes/thememanager.h
create mode 100644 test/integration/thememanagertest.cpp
create mode 100644 test/integration/thememanagertest.h
create mode 100644 test/unit/thememanagertest.cpp
create mode 100644 test/unit/thememanagertest.h
diff --git a/app-static/app-static.pro b/app-static/app-static.pro
index 4fd70850..7a1a16cc 100644
--- a/app-static/app-static.pro
+++ b/app-static/app-static.pro
@@ -24,6 +24,7 @@ SOURCES += \
themes/jsonthemetranslator.cpp \
themes/theme.cpp \
themes/themecollection.cpp \
+ themes/thememanager.cpp \
completionlistmodel.cpp \
datalocation.cpp \
slidelinemapping.cpp \
@@ -50,6 +51,7 @@ HEADERS += \
themes/jsonthemetranslatorfactory.h \
themes/theme.h \
themes/themecollection.h \
+ themes/thememanager.h \
completionlistmodel.h \
datalocation.h \
slidelinemapping.h \
diff --git a/app-static/themes/thememanager.cpp b/app-static/themes/thememanager.cpp
new file mode 100644
index 00000000..4f8ef6b4
--- /dev/null
+++ b/app-static/themes/thememanager.cpp
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "thememanager.h"
+
+#include
+#include
+
+
+static const QMap BUILTIN_MARKDOWN_HIGHLIGHTINGS = {
+ { "Default", "default" },
+ { "Solarized Light", "solarized-light" },
+ { "Solarized Dark", "solarized-dark" },
+ { "Clearness Dark", "clearness-dark" },
+ { "Byword Dark", "byword-dark" }
+};
+
+static const QMap BUILTIN_CODE_HIGHLIGHTINGS = {
+ { "Default", "default" },
+ { "Github", "github" },
+ { "Solarized Light", "solarized_light" },
+ { "Solarized Dark", "solarized_dark" }
+};
+
+static const QMap BUILTIN_PREVIEW_STYLESHEETS = {
+ { "Default", "qrc:/css/markdown.css" },
+ { "Github", "qrc:/css/github.css" },
+ { "Solarized Light", "qrc:/css/solarized-light.css" },
+ { "Solarized Dark", "qrc:/css/solarized-dark.css" },
+ { "Clearness", "qrc:/css/clearness.css" },
+ { "Clearness Dark", "qrc:/css/clearness-dark.css" },
+ { "Byword Dark", "qrc:/css/byword-dark.css" }
+};
+
+ThemeCollection ThemeManager::m_htmlPreviewThemes;
+
+
+ThemeManager::ThemeManager() :
+ ThemeManager(":/builtin-htmlpreview-themes.json")
+{
+}
+
+ThemeManager::ThemeManager(const QString &themeFileName)
+{
+ JsonFile::load(themeFileName, &m_htmlPreviewThemes);
+}
+
+Theme ThemeManager::themeByName(const QString &name) const
+{
+ return m_htmlPreviewThemes.theme(name);
+}
+
+QStringList ThemeManager::themeNames() const
+{
+ return m_htmlPreviewThemes.themeNames();
+}
+
+QString ThemeManager::markdownHighlightingPath(const Theme &theme)
+{
+ return BUILTIN_MARKDOWN_HIGHLIGHTINGS[theme.markdownHighlighting()];
+}
+
+QString ThemeManager::codeHighlightingPath(const Theme &theme)
+{
+ return BUILTIN_CODE_HIGHLIGHTINGS[theme.codeHighlighting()];
+}
+
+QString ThemeManager::previewStylesheetPath(const Theme &theme)
+{
+ return BUILTIN_PREVIEW_STYLESHEETS[theme.previewStylesheet()];
+}
diff --git a/app-static/themes/thememanager.h b/app-static/themes/thememanager.h
new file mode 100644
index 00000000..1c2ab4da
--- /dev/null
+++ b/app-static/themes/thememanager.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 THEMEMANAGER_H
+#define THEMEMANAGER_H
+
+#include
+#include
+#include "theme.h"
+#include "themecollection.h"
+
+
+class ThemeManager
+{
+public:
+ ThemeManager();
+ explicit ThemeManager(const QString &themeFileName);
+
+ Theme themeByName(const QString &name) const;
+ QStringList themeNames() const;
+
+ static QString markdownHighlightingPath(const Theme &theme);
+ static QString codeHighlightingPath(const Theme &theme);
+ static QString previewStylesheetPath(const Theme &theme);
+
+private:
+ static ThemeCollection m_htmlPreviewThemes;
+};
+
+#endif // THEMEMANAGER_H
+
diff --git a/test/integration/integration.pro b/test/integration/integration.pro
index 7936f8ba..5e0a0f35 100644
--- a/test/integration/integration.pro
+++ b/test/integration/integration.pro
@@ -18,7 +18,8 @@ SOURCES += \
jsonthemefiletest.cpp \
main.cpp \
pmhmarkdownparsertest.cpp \
- revealmarkdownconvertertest.cpp
+ revealmarkdownconvertertest.cpp \
+ thememanagertest.cpp
HEADERS += \
discountmarkdownconvertertest.h \
@@ -27,7 +28,8 @@ HEADERS += \
jsonsnippetfiletest.h \
jsonthemefiletest.h \
pmhmarkdownparsertest.h \
- revealmarkdownconvertertest.h
+ revealmarkdownconvertertest.h \
+ thememanagertest.h
target.CONFIG += no_default_install
diff --git a/test/integration/main.cpp b/test/integration/main.cpp
index 477668f4..0ce05c39 100644
--- a/test/integration/main.cpp
+++ b/test/integration/main.cpp
@@ -25,6 +25,7 @@
#include "jsonthemefiletest.h"
#include "pmhmarkdownparsertest.h"
#include "revealmarkdownconvertertest.h"
+#include "thememanagertest.h"
#ifdef ENABLE_HOEDOWN
#include "hoedownmarkdownconvertertest.h"
@@ -62,6 +63,9 @@ int main(int argc, char *argv[])
JsonThemeFileTest test8;
ret += QTest::qExec(&test8, argc, argv);
+ ThemeManagerTest test9;
+ ret += QTest::qExec(&test9, argc, argv);
+
return ret;
}
diff --git a/test/integration/thememanagertest.cpp b/test/integration/thememanagertest.cpp
new file mode 100644
index 00000000..26828b74
--- /dev/null
+++ b/test/integration/thememanagertest.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "thememanagertest.h"
+
+#include
+
+#include
+
+void ThemeManagerTest::initTestCase()
+{
+ QTemporaryFile themeFile(this);
+ if (!themeFile.open())
+ QFAIL("Failed to create temporary theme file");
+
+ QTextStream out(&themeFile);
+ out << "{ \"themes\": ["
+ << " { \"name\": \"default\","
+ << " \"markdownHighlighting\": \"default\","
+ << " \"codeHighlighting\": \"default\","
+ << " \"previewStylesheet\": \"default\" },"
+ << " { \"name\": \"dark\","
+ << " \"markdownHighlighting\": \"dark\","
+ << " \"codeHighlighting\": \"black\","
+ << " \"previewStylesheet\": \"dark\" } ] }";
+
+ themeFile.close();
+
+ themeManager = new ThemeManager(themeFile.fileName());
+}
+
+void ThemeManagerTest::cleanupTestCase()
+{
+ delete themeManager;
+}
+
+void ThemeManagerTest::returnsBuiltinThemeByName()
+{
+ Theme actual = themeManager->themeByName("dark");
+
+ QCOMPARE(actual.name(), QLatin1String("dark"));
+ QCOMPARE(actual.markdownHighlighting(), QLatin1String("dark"));
+ QCOMPARE(actual.codeHighlighting(), QLatin1String("black"));
+ QCOMPARE(actual.previewStylesheet(), QLatin1String("dark"));
+}
+
+void ThemeManagerTest::returnsNameOfAllBuiltinThemes()
+{
+ QStringList themeNames = themeManager->themeNames();
+
+ QCOMPARE(themeNames.count(), 2);
+ QCOMPARE(themeNames.at(0), QLatin1String("default"));
+ QCOMPARE(themeNames.at(1), QLatin1String("dark"));
+}
+
diff --git a/test/integration/thememanagertest.h b/test/integration/thememanagertest.h
new file mode 100644
index 00000000..0485acbc
--- /dev/null
+++ b/test/integration/thememanagertest.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 THEMEMANAGERTEST_H
+#define THEMEMANAGERTEST_H
+
+#include
+class ThemeManager;
+
+
+class ThemeManagerTest : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void returnsBuiltinThemeByName();
+ void returnsNameOfAllBuiltinThemes();
+
+private:
+ ThemeManager *themeManager;
+};
+
+#endif // THEMEMANAGERTEST_H
+
+
diff --git a/test/unit/main.cpp b/test/unit/main.cpp
index c271ee57..8e29a615 100644
--- a/test/unit/main.cpp
+++ b/test/unit/main.cpp
@@ -25,6 +25,7 @@
#include "completionlistmodeltest.h"
#include "snippettest.h"
#include "themecollectiontest.h"
+#include "thememanagertest.h"
#include "themetest.h"
#include "yamlheadercheckertest.h"
@@ -62,8 +63,11 @@ int main(int argc, char *argv[])
ThemeCollectionTest test10;
ret += QTest::qExec(&test10, argc, argv);
- JsonThemeTranslatorTest test11;
+ ThemeManagerTest test11;
ret += QTest::qExec(&test11, argc, argv);
+ JsonThemeTranslatorTest test12;
+ ret += QTest::qExec(&test12, argc, argv);
+
return ret;
}
diff --git a/test/unit/thememanagertest.cpp b/test/unit/thememanagertest.cpp
new file mode 100644
index 00000000..fdcc0046
--- /dev/null
+++ b/test/unit/thememanagertest.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 "thememanagertest.h"
+
+#include
+
+#include
+
+static const Theme defaultTheme("Default", "Default", "Default", "Default");
+static const Theme githubTheme("Github", "Github", "Github", "Github");
+static const Theme solarizedLightTheme("Solarized Light", "Solarized Light", "Solarized Light", "Solarized Light");
+static const Theme solarizedDarkTheme("Solarized Dark", "Solarized Dark", "Solarized Dark", "Solarized Dark");
+static const Theme clearnessTheme("Clearness", "Clearness", "Clearness", "Clearness");
+static const Theme clearnessDarkTheme("Clearness Dark", "Clearness Dark", "Clearness Dark", "Clearness Dark");
+static const Theme bywordDarkTheme("Byword Dark", "Byword Dark", "Byword Dark", "Byword Dark");
+
+void ThemeManagerTest::returnsPathForMarkdownHighlighting()
+{
+ ThemeManager themeManager;
+
+ QCOMPARE(themeManager.markdownHighlightingPath(defaultTheme), QLatin1String("default"));
+ QCOMPARE(themeManager.markdownHighlightingPath(solarizedLightTheme), QLatin1String("solarized-light"));
+ QCOMPARE(themeManager.markdownHighlightingPath(solarizedDarkTheme), QLatin1String("solarized-dark"));
+ QCOMPARE(themeManager.markdownHighlightingPath(clearnessDarkTheme), QLatin1String("clearness-dark"));
+ QCOMPARE(themeManager.markdownHighlightingPath(bywordDarkTheme), QLatin1String("byword-dark"));
+}
+
+void ThemeManagerTest::returnsPathForCodeHighlighting()
+{
+ ThemeManager themeManager;
+
+ QCOMPARE(themeManager.codeHighlightingPath(defaultTheme), QLatin1String("default"));
+ QCOMPARE(themeManager.codeHighlightingPath(githubTheme), QLatin1String("github"));
+ QCOMPARE(themeManager.codeHighlightingPath(solarizedLightTheme), QLatin1String("solarized_light"));
+ QCOMPARE(themeManager.codeHighlightingPath(solarizedDarkTheme), QLatin1String("solarized_dark"));
+}
+
+void ThemeManagerTest::returnsPathForPreviewStylesheet()
+{
+ ThemeManager themeManager;
+
+ QCOMPARE(themeManager.previewStylesheetPath(defaultTheme), QLatin1String("qrc:/css/markdown.css"));
+ QCOMPARE(themeManager.previewStylesheetPath(githubTheme), QLatin1String("qrc:/css/github.css"));
+ QCOMPARE(themeManager.previewStylesheetPath(solarizedLightTheme), QLatin1String("qrc:/css/solarized-light.css"));
+ QCOMPARE(themeManager.previewStylesheetPath(solarizedDarkTheme), QLatin1String("qrc:/css/solarized-dark.css"));
+ QCOMPARE(themeManager.previewStylesheetPath(clearnessTheme), QLatin1String("qrc:/css/clearness.css"));
+ QCOMPARE(themeManager.previewStylesheetPath(clearnessDarkTheme), QLatin1String("qrc:/css/clearness-dark.css"));
+ QCOMPARE(themeManager.previewStylesheetPath(bywordDarkTheme), QLatin1String("qrc:/css/byword-dark.css"));
+}
+
diff --git a/test/unit/thememanagertest.h b/test/unit/thememanagertest.h
new file mode 100644
index 00000000..72d9e8fa
--- /dev/null
+++ b/test/unit/thememanagertest.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 Christian Loose
+ *
+ * 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 2 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 THEMEMANAGERTEST_H
+#define THEMEMANAGERTEST_H
+
+#include
+
+class ThemeManagerTest : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void returnsPathForMarkdownHighlighting();
+ void returnsPathForCodeHighlighting();
+ void returnsPathForPreviewStylesheet();
+};
+
+#endif // THEMEMANAGERTEST_H
+
+
diff --git a/test/unit/unit.pro b/test/unit/unit.pro
index ca584408..7f436e33 100644
--- a/test/unit/unit.pro
+++ b/test/unit/unit.pro
@@ -21,7 +21,8 @@ SOURCES += \
dictionarytest.cpp \
yamlheadercheckertest.cpp \
themetest.cpp \
- themecollectiontest.cpp
+ themecollectiontest.cpp \
+ thememanagertest.cpp
HEADERS += \
completionlistmodeltest.h \
@@ -34,7 +35,8 @@ HEADERS += \
dictionarytest.h \
yamlheadercheckertest.h \
themetest.h \
- themecollectiontest.h
+ themecollectiontest.h \
+ thememanagertest.h
target.CONFIG += no_default_install
From 10d74301b0adec063f7fc73331080d00357468ea Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Thu, 10 Dec 2015 19:52:28 +0100
Subject: [PATCH 18/74] Remove specific style actions and use ThemeManager
instead
Remove the specific style actions from the main window and use the
ThemeManager instead to fill the Styles submenu.
Also rename option lastUsedStyle to lastUsedTheme.
---
app/mainwindow.cpp | 132 ++++++++++++++++++++-------------------------
app/mainwindow.h | 17 +++---
app/mainwindow.ui | 7 ---
app/options.cpp | 45 ++++++++++++----
app/options.h | 11 ++--
5 files changed, 110 insertions(+), 102 deletions(-)
diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp
index a4d79707..5b7fc4e0 100644
--- a/app/mainwindow.cpp
+++ b/app/mainwindow.cpp
@@ -47,6 +47,7 @@
#include
#include
#include
+#include
#include
#include "controls/activelabel.h"
#include "controls/findreplacewidget.h"
@@ -78,6 +79,7 @@ MainWindow::MainWindow(const QString &fileName, QWidget *parent) :
snippetCollection(new SnippetCollection(this)),
viewSynchronizer(0),
htmlPreviewController(0),
+ themeManager(new ThemeManager()),
splitFactor(0.5),
rightViewCollapsed(false)
{
@@ -124,8 +126,10 @@ void MainWindow::initializeApp()
ui->webView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
ui->tocWebView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
- // set last used style
- lastUsedStyle();
+ setupHtmlPreviewThemes();
+
+ // apply last used theme
+ lastUsedTheme();
ui->plainTextEdit->tabWidthChanged(options->tabWidth());
@@ -464,97 +468,61 @@ void MainWindow::viewChangeSplit()
}
}
-void MainWindow::lastUsedStyle()
+void MainWindow::lastUsedTheme()
{
- if (stylesGroup) {
- foreach(QAction *action, stylesGroup->actions()) {
- if (action->objectName() == options->lastUsedStyle()) {
- action->trigger();
- }
- }
- }
-}
+ QString themeName = options->lastUsedTheme();
-void MainWindow::styleDefault()
-{
- generator->setCodeHighlightingStyle("default");
- ui->plainTextEdit->loadStyleFromStylesheet(stylePath("default"));
- ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl("qrc:/css/markdown.css"));
+ currentTheme = themeManager->themeByName(themeName);
+ applyCurrentTheme();
- styleLabel->setText(ui->actionDefault->text());
- options->setLastUsedStyle(ui->actionDefault->objectName());
+ styleLabel->setText(themeName);
}
-void MainWindow::styleGithub()
+void MainWindow::themeChanged()
{
- generator->setCodeHighlightingStyle("github");
- ui->plainTextEdit->loadStyleFromStylesheet(stylePath("default"));
- ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl("qrc:/css/github.css"));
-
- styleLabel->setText(ui->actionGithub->text());
- options->setLastUsedStyle(ui->actionGithub->objectName());
-}
+ QAction *action = qobject_cast(sender());
+ QString themeName = action->text();
-void MainWindow::styleSolarizedLight()
-{
- generator->setCodeHighlightingStyle("solarized_light");
- ui->plainTextEdit->loadStyleFromStylesheet(stylePath("solarized-light"));
- ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl("qrc:/css/solarized-light.css"));
+ currentTheme = themeManager->themeByName(themeName);
+ applyCurrentTheme();
- styleLabel->setText(ui->actionSolarizedLight->text());
- options->setLastUsedStyle(ui->actionSolarizedLight->objectName());
+ styleLabel->setText(themeName);
+ options->setLastUsedTheme(themeName);
}
-void MainWindow::styleSolarizedDark()
+void MainWindow::editorStyleChanged()
{
- generator->setCodeHighlightingStyle("solarized_dark");
- ui->plainTextEdit->loadStyleFromStylesheet(stylePath("solarized-dark"));
- ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl("qrc:/css/solarized-dark.css"));
-
- styleLabel->setText(ui->actionSolarizedDark->text());
- options->setLastUsedStyle(ui->actionSolarizedDark->objectName());
+ QString markdownHighlighting = ThemeManager::markdownHighlightingPath(currentTheme);
+ ui->plainTextEdit->loadStyleFromStylesheet(stylePath(markdownHighlighting));
}
-void MainWindow::styleClearness()
+void MainWindow::styleCustomStyle()
{
- generator->setCodeHighlightingStyle("default");
- ui->plainTextEdit->loadStyleFromStylesheet(stylePath("default"));
- ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl("qrc:/css/clearness.css"));
+ QAction *action = qobject_cast(sender());
- styleLabel->setText(ui->actionClearness->text());
- options->setLastUsedStyle(ui->actionClearness->objectName());
-}
+ currentTheme = { action->text(), "Default", "Default", action->data().toString() };
-void MainWindow::styleClearnessDark()
-{
- generator->setCodeHighlightingStyle("default");
- ui->plainTextEdit->loadStyleFromStylesheet(stylePath("clearness-dark"));
- ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl("qrc:/css/clearness-dark.css"));
+ QString markdownHighlighting = ThemeManager::markdownHighlightingPath(currentTheme);
+ QString codeHighlighting = ThemeManager::codeHighlightingPath(currentTheme);
+ QString previewStylesheet = ThemeManager::previewStylesheetPath(currentTheme);
- styleLabel->setText(ui->actionClearnessDark->text());
- options->setLastUsedStyle(ui->actionClearnessDark->objectName());
-}
+ generator->setCodeHighlightingStyle(codeHighlighting);
+ ui->plainTextEdit->loadStyleFromStylesheet(stylePath(markdownHighlighting));
+ ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl::fromLocalFile(previewStylesheet));
-void MainWindow::styleBywordDark()
-{
- generator->setCodeHighlightingStyle("default");
- ui->plainTextEdit->loadStyleFromStylesheet(stylePath("byword-dark"));
- ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl("qrc:/css/byword-dark.css"));
-
- styleLabel->setText(ui->actionBywordDark->text());
- options->setLastUsedStyle(ui->actionBywordDark->objectName());
+ styleLabel->setText(action->text());
+ options->setLastUsedTheme(action->objectName());
}
-void MainWindow::styleCustomStyle()
+void MainWindow::applyCurrentTheme()
{
- QAction *action = qobject_cast(sender());
-
- generator->setCodeHighlightingStyle("default");
- ui->plainTextEdit->loadStyleFromStylesheet(stylePath("default"));
- ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl::fromLocalFile(action->data().toString()));
+ QString markdownHighlighting = ThemeManager::markdownHighlightingPath(currentTheme);
+ QString codeHighlighting = ThemeManager::codeHighlightingPath(currentTheme);
+ QString previewStylesheet = ThemeManager::previewStylesheetPath(currentTheme);
- styleLabel->setText(action->text());
- options->setLastUsedStyle(action->objectName());
+ generator->setCodeHighlightingStyle(codeHighlighting);
+ ui->plainTextEdit->loadStyleFromStylesheet(stylePath(markdownHighlighting));
+ ui->webView->page()->settings()->setUserStyleSheetUrl(QUrl(previewStylesheet));
}
void MainWindow::viewFullScreenMode()
@@ -949,8 +917,8 @@ void MainWindow::setupUi()
this, SLOT(proxyConfigurationChanged()));
connect(options, SIGNAL(markdownConverterChanged()),
this, SLOT(markdownConverterChanged()));
- connect(options, SIGNAL(editorFontChanged(QFont)),
- this, SLOT(lastUsedStyle()));
+ connect(options, &Options::editorStyleChanged,
+ this, &MainWindow::editorStyleChanged);
readSettings();
setupCustomShortcuts();
@@ -1231,6 +1199,24 @@ void MainWindow::updateSplitter()
ui->splitter->setSizes(childSizes);
}
+void MainWindow::setupHtmlPreviewThemes()
+{
+ ui->menuStyles->clear();
+
+ delete stylesGroup;
+ stylesGroup = new QActionGroup(this);
+
+ int key = 1;
+ foreach(const QString &themeName, themeManager->themeNames()) {
+ QAction *action = ui->menuStyles->addAction(themeName);
+ action->setShortcut(QKeySequence(tr("Ctrl+%1").arg(key++)));
+ action->setCheckable(true);
+ action->setActionGroup(stylesGroup);
+ connect(action, &QAction::triggered,
+ this, &MainWindow::themeChanged);
+ }
+}
+
void MainWindow::loadCustomStyles()
{
QStringList paths = DataLocation::standardLocations();
diff --git a/app/mainwindow.h b/app/mainwindow.h
index 9e001a71..23f15b68 100644
--- a/app/mainwindow.h
+++ b/app/mainwindow.h
@@ -20,6 +20,7 @@
#include
#include
#include
+#include
namespace Ui {
class MainWindow;
@@ -37,6 +38,7 @@ class RecentFilesMenu;
class Options;
class SlideLineMapping;
class SnippetCollection;
+class ThemeManager;
class ViewSynchronizer;
@@ -83,14 +85,9 @@ private slots:
void editInsertImage();
void viewChangeSplit();
- void lastUsedStyle();
- void styleDefault();
- void styleGithub();
- void styleSolarizedLight();
- void styleSolarizedDark();
- void styleClearness();
- void styleClearnessDark();
- void styleBywordDark();
+ void lastUsedTheme();
+ void themeChanged();
+ void editorStyleChanged();
void styleCustomStyle();
void viewFullScreenMode();
void viewHorizontalLayout(bool checked);
@@ -143,9 +140,11 @@ private slots:
bool maybeSave();
void setFileName(const QString &fileName);
void updateSplitter();
+ void setupHtmlPreviewThemes();
void loadCustomStyles();
void readSettings();
void writeSettings();
+ void applyCurrentTheme();
QString stylePath(const QString &styleName);
private:
@@ -161,6 +160,8 @@ private slots:
SnippetCollection *snippetCollection;
ViewSynchronizer *viewSynchronizer;
HtmlPreviewController *htmlPreviewController;
+ ThemeManager *themeManager;
+ Theme currentTheme { "Default", "Default", "Default", "Default" };
QString fileName;
float splitFactor;
bool rightViewCollapsed;
diff --git a/app/mainwindow.ui b/app/mainwindow.ui
index 0de78cd1..a2a30a71 100644
--- a/app/mainwindow.ui
+++ b/app/mainwindow.ui
@@ -258,13 +258,6 @@
St&yles
-
-
-
-
-
-
-
@@ -180,7 +186,7 @@ p, li { white-space: pre-wrap; }
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
+</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:7.8pt; font-weight:400; font-style:normal;">
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt; font-weight:600;">Qt</span><span style=" font-size:8pt;"> (LGPL v2.1)</span></p>
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a href="http://qt-project.org/"><span style=" font-size:8pt; text-decoration: underline; color:#0000ff;">http://qt.io/</span></a></p>
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;"><br /></p>
From 5f7292e38a7152950c137c3ab2fa6f10e4e1886f Mon Sep 17 00:00:00 2001
From: "Walter.Liu"
Date: Tue, 1 Dec 2015 23:02:52 +0800
Subject: [PATCH 35/74] [#257] Adopt "General/foo" instead of "general/foo". It
may be the Qt5 bug that stores "general" section as "[%General]",not
"[%general]" in NativeFormat on Linux. It will not bother windows user(s)
anyhow, since the Windows registry use case-insensitive keys. This should
fix #257 and #249.
---
app/options.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/options.cpp b/app/options.cpp
index b772be01..9f4ad6ca 100644
--- a/app/options.cpp
+++ b/app/options.cpp
@@ -20,8 +20,8 @@
#include
-static const char* MARKDOWN_CONVERTER = "general/converter";
-static const char* LAST_USED_STYLE = "general/lastusedstyle";
+static const char* MARKDOWN_CONVERTER = "General/converter";
+static const char* LAST_USED_STYLE = "General/lastusedstyle";
static const char* STYLE_DEFAULT = "actionDefault";
static const char* FONT_FAMILY_DEFAULT = "Monospace";
static const char* FONT_FAMILY = "editor/font/family";
From 43146ac63c27e7c2282652bee7c7effd9a5c04db Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Fri, 1 Jan 2016 18:58:30 +0100
Subject: [PATCH 36/74] Update README for v0.11.2
---
README.md | 24 ++++++++++++++++++++----
1 file changed, 20 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 75c3bfbf..6cd17385 100644
--- a/README.md
+++ b/README.md
@@ -8,15 +8,31 @@ A Qt-based, free and open source markdown editor with live HTML preview, math ex
### DOWNLOAD
-[Sources](https://github.com/cloose/CuteMarkEd/archive/v0.11.1.tar.gz)
-[MS Windows (Installer)](http://dl.bintray.com/cloose/CuteMarkEd/cutemarked-0.11.1.msi)
-[MS Windows (ZIP file)](http://dl.bintray.com/cloose/CuteMarkEd/cutemarked-0.11.1.zip)
-[OpenSUSE 13.1 (RPM)](https://build.opensuse.org/project/show?project=home%3Acloose1974)
+[Sources](https://github.com/cloose/CuteMarkEd/archive/v0.11.2.tar.gz)
+[MS Windows (Installer)](http://dl.bintray.com/cloose/CuteMarkEd/cutemarked-0.11.2.msi)
+[MS Windows (ZIP file)](http://dl.bintray.com/cloose/CuteMarkEd/cutemarked-0.11.2.zip)
+[OpenSUSE 13.2 (RPM)](https://build.opensuse.org/project/show?project=home%3Acloose1974)
[Fedora 20 (RPM)](https://build.opensuse.org/project/show?project=home%3Acloose1974)
[Fedora 21 (RPM)](https://build.opensuse.org/project/show?project=home%3Acloose1974)
+[Fedora 22 (RPM)](https://build.opensuse.org/project/show?project=home%3Acloose1974)
+[Fedora 23 (RPM)](https://build.opensuse.org/project/show?project=home%3Acloose1974)
### NEWS
+#### Version 0.11.2
+
+Improvements:
+
+* `IMPROVED` Added Hungarian translation
+
+Fixes:
+
+* `FIXED` Editor pane jumping up and down during editing (#232)
+* `FIXED` Missing mermaid CSS for styling in preview (#241)
+* `FIXED` Correct order of HTML Preview/Source menu item (#242)
+* `FIXED` Retrieval of last used style on application start on Linux (#257)
+* `FIXED` Crash when switching between markdown converters (#260)
+
#### Version 0.11.1
Improvements:
From 918bb4819833cd3d4af15a522fbe97135c1b4ee6 Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Mon, 4 Jan 2016 09:16:51 +0100
Subject: [PATCH 37/74] Fix build error after merge of hotfix/v0.11.2
---
app/options.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/options.cpp b/app/options.cpp
index ca6d44ae..2237ed1d 100644
--- a/app/options.cpp
+++ b/app/options.cpp
@@ -20,7 +20,7 @@
static const char* MARKDOWN_CONVERTER = "General/converter";
-static const char* LAST_USED_STYLE = "General/lastusedstyle";
+static const char* LAST_USED_THEME = "General/lastusedstyle";
static const char* THEME_DEFAULT = "Default";
static const char* FONT_FAMILY_DEFAULT = "Monospace";
static const char* FONT_FAMILY = "editor/font/family";
From 4b4202779107a8cb79e8a009b0bd9600b84c04c6 Mon Sep 17 00:00:00 2001
From: Christian Loose
Date: Sun, 10 Jan 2016 13:04:37 +0100
Subject: [PATCH 38/74] New option is called lastusedtheme not lastusedstyle
---
app/options.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/options.cpp b/app/options.cpp
index 2237ed1d..a9e9f5a3 100644
--- a/app/options.cpp
+++ b/app/options.cpp
@@ -20,7 +20,7 @@
static const char* MARKDOWN_CONVERTER = "General/converter";
-static const char* LAST_USED_THEME = "General/lastusedstyle";
+static const char* LAST_USED_THEME = "General/lastusedtheme";
static const char* THEME_DEFAULT = "Default";
static const char* FONT_FAMILY_DEFAULT = "Monospace";
static const char* FONT_FAMILY = "editor/font/family";
From c2d9c8df87ed5417ffee839c1488bc65a443dc39 Mon Sep 17 00:00:00 2001
From: Kolcha
Date: Fri, 29 Jan 2016 12:43:10 +0300
Subject: [PATCH 39/74] added OS X specific resources
---
app/Info.plist | 26 ++++++++++++++++++++++++++
app/app-icon.icns | Bin 0 -> 499032 bytes
app/app.pro | 6 +++++-
app/hunspell/spellchecker_unix.cpp | 7 +++++--
4 files changed, 36 insertions(+), 3 deletions(-)
create mode 100644 app/Info.plist
create mode 100644 app/app-icon.icns
diff --git a/app/Info.plist b/app/Info.plist
new file mode 100644
index 00000000..b5e472f2
--- /dev/null
+++ b/app/Info.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleName
+ CuteMarkEd
+ CFBundleIconFile
+ app-icon.icns
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 0.11.2
+ CFBundleSignature
+ CMDE
+ CFBundleExecutable
+ cutemarked
+ CFBundleIdentifier
+ io.github.cloose.CuteMarkEd
+ NSPrincipalClass
+ NSApplication
+ NSHighResolutionCapable
+ True
+ NSHumanReadableCopyright
+ © 2013-2016 Christian Loose
+
+
diff --git a/app/app-icon.icns b/app/app-icon.icns
new file mode 100644
index 0000000000000000000000000000000000000000..58cabb79138f442224c01e0c096c30e1708799ce
GIT binary patch
literal 499032
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