diff --git a/iconos.qrc b/iconos.qrc
new file mode 100644
index 0000000..fb8ab4f
--- /dev/null
+++ b/iconos.qrc
@@ -0,0 +1,17 @@
+
+
+ images/insertimage.png
+ images/simplifyrichtext.png
+ images/textanchor.png
+ images/textbold.png
+ images/textcenter.png
+ images/textitalic.png
+ images/textjustify.png
+ images/textleft.png
+ images/textright.png
+ images/textsubscript.png
+ images/textsuperscript.png
+ images/textunder.png
+ images/righttoleft.png
+
+
diff --git a/images/insertimage.png b/images/insertimage.png
new file mode 100644
index 0000000..cfab637
Binary files /dev/null and b/images/insertimage.png differ
diff --git a/images/righttoleft.png b/images/righttoleft.png
new file mode 100644
index 0000000..7590664
Binary files /dev/null and b/images/righttoleft.png differ
diff --git a/images/simplifyrichtext.png b/images/simplifyrichtext.png
new file mode 100644
index 0000000..e251cf7
Binary files /dev/null and b/images/simplifyrichtext.png differ
diff --git a/images/textanchor.png b/images/textanchor.png
new file mode 100644
index 0000000..1911ab0
Binary files /dev/null and b/images/textanchor.png differ
diff --git a/images/textbold.png b/images/textbold.png
new file mode 100644
index 0000000..9cbc713
Binary files /dev/null and b/images/textbold.png differ
diff --git a/images/textcenter.png b/images/textcenter.png
new file mode 100644
index 0000000..11efb4b
Binary files /dev/null and b/images/textcenter.png differ
diff --git a/images/textitalic.png b/images/textitalic.png
new file mode 100644
index 0000000..b30ce14
Binary files /dev/null and b/images/textitalic.png differ
diff --git a/images/textjustify.png b/images/textjustify.png
new file mode 100644
index 0000000..9de0c88
Binary files /dev/null and b/images/textjustify.png differ
diff --git a/images/textleft.png b/images/textleft.png
new file mode 100644
index 0000000..16f80bc
Binary files /dev/null and b/images/textleft.png differ
diff --git a/images/textright.png b/images/textright.png
new file mode 100644
index 0000000..16872df
Binary files /dev/null and b/images/textright.png differ
diff --git a/images/textsubscript.png b/images/textsubscript.png
new file mode 100644
index 0000000..d86347d
Binary files /dev/null and b/images/textsubscript.png differ
diff --git a/images/textsuperscript.png b/images/textsuperscript.png
new file mode 100644
index 0000000..9109965
Binary files /dev/null and b/images/textsuperscript.png differ
diff --git a/images/textunder.png b/images/textunder.png
new file mode 100644
index 0000000..c72eff5
Binary files /dev/null and b/images/textunder.png differ
diff --git a/include/addlinkdialog.h b/include/addlinkdialog.h
new file mode 100644
index 0000000..163d354
--- /dev/null
+++ b/include/addlinkdialog.h
@@ -0,0 +1,27 @@
+#ifndef ADDLINKDIALOG_H
+#define ADDLINKDIALOG_H
+
+#include "ui_addlinkdialog.h"
+#include "richtexteditor.h"
+
+#include
+
+class AddLinkDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ AddLinkDialog(RichTextEditor *editor, QWidget *parent = nullptr);
+ ~AddLinkDialog() override;
+
+ int showDialog();
+
+public Q_SLOTS:
+ void accept() override;
+
+private:
+ RichTextEditor *m_editor;
+ QT_PREPEND_NAMESPACE(Ui)::AddLinkDialog *m_ui;
+};
+
+#endif // ADDLINKDIALOG_H
diff --git a/include/coloraction.h b/include/coloraction.h
new file mode 100644
index 0000000..0ff9d1e
--- /dev/null
+++ b/include/coloraction.h
@@ -0,0 +1,26 @@
+#ifndef COLORACTION_H
+#define COLORACTION_H
+
+#include
+#include
+
+class ColorAction : public QAction
+{
+ Q_OBJECT
+
+public:
+ ColorAction(QObject *parent);
+
+ const QColor& color() const { return m_color; }
+ void setColor(const QColor &color);
+
+Q_SIGNALS:
+ void colorChanged(const QColor &color);
+
+private Q_SLOTS:
+ void chooseColor();
+
+private:
+ QColor m_color;
+};
+#endif // COLORACTION_H
diff --git a/include/htmlhighlighter.h b/include/htmlhighlighter.h
new file mode 100644
index 0000000..1601ab1
--- /dev/null
+++ b/include/htmlhighlighter.h
@@ -0,0 +1,43 @@
+#ifndef HTMLHIGHLIGHTER_H
+#define HTMLHIGHLIGHTER_H
+
+#include
+#include
+#include
+
+/* HTML syntax highlighter based on Qt Quarterly example */
+class HtmlHighlighter : public QSyntaxHighlighter
+{
+ Q_OBJECT
+
+public:
+ enum Construct {
+ Entity,
+ Tag,
+ Comment,
+ Attribute,
+ Value,
+ LastConstruct = Value
+ };
+
+ HtmlHighlighter(QTextEdit *textEdit);
+
+ void setFormatFor(Construct construct, const QTextCharFormat &format);
+
+ QTextCharFormat formatFor(Construct construct) const
+ { return m_formats[construct]; }
+
+protected:
+ enum State {
+ NormalState = -1,
+ InComment,
+ InTag
+ };
+
+ void highlightBlock(const QString &text) override;
+
+private:
+ QTextCharFormat m_formats[LastConstruct + 1];
+};
+
+#endif // HTMLHIGHLIGHTER_H
diff --git a/include/htmltextedit.h b/include/htmltextedit.h
new file mode 100644
index 0000000..b12f3e6
--- /dev/null
+++ b/include/htmltextedit.h
@@ -0,0 +1,23 @@
+#ifndef HTMLTEXTEDIT_H
+#define HTMLTEXTEDIT_H
+
+#include
+#include
+#include
+
+class HtmlTextEdit : public QTextEdit
+{
+ Q_OBJECT
+
+public:
+ HtmlTextEdit(QWidget *parent = nullptr)
+ : QTextEdit(parent)
+ {}
+
+ void contextMenuEvent(QContextMenuEvent *event) override;
+
+private slots:
+ void actionTriggered(QAction *action);
+};
+
+#endif // HTMLTEXTEDIT_H
diff --git a/include/richtexteditor.h b/include/richtexteditor.h
new file mode 100644
index 0000000..a9b5d16
--- /dev/null
+++ b/include/richtexteditor.h
@@ -0,0 +1,34 @@
+#ifndef RICHTEXTEDITOR_H
+#define RICHTEXTEDITOR_H
+
+#include
+#include
+
+class RichTextEditor : public QTextEdit
+{
+ Q_OBJECT
+public:
+ explicit RichTextEditor(QWidget *parent = nullptr);
+ void setDefaultFont(QFont font);
+
+ QToolBar *createToolBar(QWidget *parent = nullptr);
+
+ QString text(Qt::TextFormat format) const;
+
+ bool simplifyRichText() const { return m_simplifyRichText; }
+
+public Q_SLOTS:
+ void setFontBold(bool b);
+ void setFontPointSize(double);
+ void setText(const QString &text);
+ void setSimplifyRichText(bool v);
+
+Q_SIGNALS:
+ void stateChanged();
+ void simplifyRichTextChanged(bool);
+
+private:
+ bool m_simplifyRichText;
+};
+
+#endif // RICHTEXTEDITOR_H
diff --git a/include/richtexteditordialog.h b/include/richtexteditordialog.h
new file mode 100644
index 0000000..1748d06
--- /dev/null
+++ b/include/richtexteditordialog.h
@@ -0,0 +1,37 @@
+#ifndef RICHTEXTEDITORDIALOG_H
+#define RICHTEXTEDITORDIALOG_H
+
+#include "richtexteditor.h"
+
+#include
+#include
+#include
+
+class RichTextEditorDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ explicit RichTextEditorDialog(QWidget *parent = nullptr);
+ ~RichTextEditorDialog();
+
+ int showDialog();
+ void setDefaultFont(const QFont &font);
+ void setText(const QString &text);
+ QString text(Qt::TextFormat format = Qt::AutoText) const;
+
+private Q_SLOTS:
+ void tabIndexChanged(int newIndex);
+ void richTextChanged();
+ void sourceChanged();
+
+private:
+ enum TabIndex { RichTextIndex, SourceIndex };
+ enum State { Clean, RichTextChanged, SourceChanged };
+ RichTextEditor *m_editor;
+ QTextEdit *m_text_edit;
+ QTabWidget *m_tab_widget;
+ State m_state;
+ int m_initialTab;
+};
+
+#endif // RICHTEXTEDITORDIALOG_H
diff --git a/include/richtexteditortoolbar.h b/include/richtexteditortoolbar.h
new file mode 100644
index 0000000..0480c83
--- /dev/null
+++ b/include/richtexteditortoolbar.h
@@ -0,0 +1,57 @@
+#ifndef RICHTEXTEDITORTOOLBAR_H
+#define RICHTEXTEDITORTOOLBAR_H
+
+#include "richtexteditor.h"
+#include "coloraction.h"
+
+#include
+#include
+#include
+#include
+#include
+
+QIcon createIconSet(const QString &name);
+
+class RichTextEditorToolBar : public QToolBar
+{
+ Q_OBJECT
+public:
+ RichTextEditorToolBar(RichTextEditor *editor,
+ QWidget *parent = nullptr);
+
+public Q_SLOTS:
+ void updateActions();
+
+private Q_SLOTS:
+ void alignmentActionTriggered(QAction *action);
+ void sizeInputActivated(const QString &size);
+ void fontFamilyActivated(const QString &font);
+ void colorChanged(const QColor &color);
+ void setVAlignSuper(bool super);
+ void setVAlignSub(bool sub);
+ void insertLink();
+ void insertImage();
+ void layoutDirectionChanged();
+
+private:
+ QAction *m_bold_action;
+ QAction *m_italic_action;
+ QAction *m_underline_action;
+ QAction *m_valign_sup_action;
+ QAction *m_valign_sub_action;
+ QAction *m_align_left_action;
+ QAction *m_align_center_action;
+ QAction *m_align_right_action;
+ QAction *m_align_justify_action;
+ QAction *m_layoutDirectionAction;
+ QAction *m_link_action;
+ QAction *m_image_action;
+ QAction *m_simplify_richtext_action;
+ ColorAction *m_color_action;
+ QComboBox *m_font_size_input;
+ QFontComboBox *m_font_family_input;
+
+ QPointer m_editor;
+};
+
+#endif // RICHTEXTEDITORTOOLBAR_H
diff --git a/src/addlinkdialog.cpp b/src/addlinkdialog.cpp
new file mode 100644
index 0000000..ee21c9c
--- /dev/null
+++ b/src/addlinkdialog.cpp
@@ -0,0 +1,45 @@
+#include "addlinkdialog.h"
+
+AddLinkDialog::AddLinkDialog(RichTextEditor *editor, QWidget *parent) :
+ QDialog(parent),
+ m_ui(new QT_PREPEND_NAMESPACE(Ui)::AddLinkDialog)
+{
+ m_ui->setupUi(this);
+
+ m_editor = editor;
+}
+
+AddLinkDialog::~AddLinkDialog()
+{
+ delete m_ui;
+}
+
+int AddLinkDialog::showDialog()
+{
+ // Set initial focus
+ const QTextCursor cursor = m_editor->textCursor();
+ if (cursor.hasSelection()) {
+ m_ui->titleInput->setText(cursor.selectedText());
+ m_ui->urlInput->setFocus();
+ } else {
+ m_ui->titleInput->setFocus();
+ }
+
+ return exec();
+}
+
+void AddLinkDialog::accept()
+{
+ const QString title = m_ui->titleInput->text();
+ const QString url = m_ui->urlInput->text();
+
+ if (!title.isEmpty()) {
+ const QString html = "" + title + "";
+ m_editor->insertHtml(html);
+ }
+
+ m_ui->titleInput->clear();
+ m_ui->urlInput->clear();
+
+ QDialog::accept();
+}
diff --git a/src/coloraction.cpp b/src/coloraction.cpp
new file mode 100644
index 0000000..af9ed7b
--- /dev/null
+++ b/src/coloraction.cpp
@@ -0,0 +1,36 @@
+#include "coloraction.h"
+
+#include
+#include
+#include
+
+ColorAction::ColorAction(QObject *parent):
+ QAction(parent)
+{
+ setText(tr("Text Color"));
+ setColor(Qt::black);
+ connect(this, &QAction::triggered, this, &ColorAction::chooseColor);
+}
+
+void ColorAction::setColor(const QColor &color)
+{
+ if (color == m_color)
+ return;
+ m_color = color;
+ QPixmap pix(24, 24);
+ QPainter painter(&pix);
+ painter.setRenderHint(QPainter::Antialiasing, false);
+ painter.fillRect(pix.rect(), m_color);
+ painter.setPen(m_color.darker());
+ painter.drawRect(pix.rect().adjusted(0, 0, -1, -1));
+ setIcon(pix);
+}
+
+void ColorAction::chooseColor()
+{
+ const QColor col = QColorDialog::getColor(m_color, nullptr);
+ if (col.isValid() && col != m_color) {
+ setColor(col);
+ emit colorChanged(m_color);
+ }
+}
diff --git a/src/htmlhighlighter.cpp b/src/htmlhighlighter.cpp
new file mode 100644
index 0000000..bbdef3b
--- /dev/null
+++ b/src/htmlhighlighter.cpp
@@ -0,0 +1,142 @@
+#include "htmlhighlighter.h"
+
+#include
+
+using namespace Qt::StringLiterals;
+
+HtmlHighlighter::HtmlHighlighter(QTextEdit *textEdit)
+ : QSyntaxHighlighter(textEdit->document())
+{
+ QTextCharFormat entityFormat;
+ entityFormat.setForeground(Qt::red);
+ setFormatFor(Entity, entityFormat);
+
+ QTextCharFormat tagFormat;
+ tagFormat.setForeground(Qt::darkMagenta);
+ tagFormat.setFontWeight(QFont::Bold);
+ setFormatFor(Tag, tagFormat);
+
+ QTextCharFormat commentFormat;
+ commentFormat.setForeground(Qt::gray);
+ commentFormat.setFontItalic(true);
+ setFormatFor(Comment, commentFormat);
+
+ QTextCharFormat attributeFormat;
+ attributeFormat.setForeground(Qt::black);
+ attributeFormat.setFontWeight(QFont::Bold);
+ setFormatFor(Attribute, attributeFormat);
+
+ QTextCharFormat valueFormat;
+ valueFormat.setForeground(Qt::blue);
+ setFormatFor(Value, valueFormat);
+}
+
+void HtmlHighlighter::setFormatFor(Construct construct,
+ const QTextCharFormat &format)
+{
+ m_formats[construct] = format;
+ rehighlight();
+}
+
+void HtmlHighlighter::highlightBlock(const QString &text)
+{
+ static const QChar tab = u'\t';
+ static const QChar space = u' ';
+
+ int state = previousBlockState();
+ qsizetype len = text.size();
+ qsizetype start = 0;
+ qsizetype pos = 0;
+
+ while (pos < len) {
+ switch (state) {
+ case NormalState:
+ default:
+ while (pos < len) {
+ QChar ch = text.at(pos);
+ if (ch == u'<') {
+ if (QStringView{text}.sliced(pos).startsWith(""_L1)) {
+ pos += 3;
+ state = NormalState;
+ break;
+ }
+ }
+ setFormat(start, pos - start, m_formats[Comment]);
+ break;
+ case InTag:
+ QChar quote = QChar::Null;
+ while (pos < len) {
+ QChar ch = text.at(pos);
+ if (quote.isNull()) {
+ start = pos;
+ if (ch == '\''_L1 || ch == u'"') {
+ quote = ch;
+ } else if (ch == u'>') {
+ ++pos;
+ setFormat(start, pos - start, m_formats[Tag]);
+ state = NormalState;
+ break;
+ } else if (QStringView{text}.sliced(pos).startsWith("/>"_L1)) {
+ pos += 2;
+ setFormat(start, pos - start, m_formats[Tag]);
+ state = NormalState;
+ break;
+ } else if (ch != space && text.at(pos) != tab) {
+ // Tag not ending, not a quote and no whitespace, so
+ // we must be dealing with an attribute.
+ ++pos;
+ while (pos < len && text.at(pos) != space
+ && text.at(pos) != tab
+ && text.at(pos) != u'=')
+ ++pos;
+ setFormat(start, pos - start, m_formats[Attribute]);
+ start = pos;
+ }
+ } else if (ch == quote) {
+ quote = QChar::Null;
+
+ // Anything quoted is a value
+ setFormat(start, pos - start, m_formats[Value]);
+ }
+ ++pos;
+ }
+ break;
+ }
+ }
+ setCurrentBlockState(state);
+}
+
diff --git a/src/htmltextedit.cpp b/src/htmltextedit.cpp
new file mode 100644
index 0000000..df97703
--- /dev/null
+++ b/src/htmltextedit.cpp
@@ -0,0 +1,40 @@
+#include "htmltextedit.h"
+
+#include
+
+void HtmlTextEdit::contextMenuEvent(QContextMenuEvent *event)
+{
+ QMenu *menu = createStandardContextMenu();
+ QMenu *htmlMenu = new QMenu(tr("Insert HTML entity"), menu);
+
+ typedef struct {
+ const char *text;
+ const char *entity;
+ } Entry;
+
+ const Entry entries[] = {
+ { "&& (&&)", "&" },
+ { "& ", " " },
+ { "&< (<)", "<" },
+ { "&> (>)", ">" },
+ { "&© (Copyright)", "©" },
+ { "&® (Trade Mark)", "®" },
+ };
+
+ for (const Entry &e : entries) {
+ QAction *entityAction = new QAction(QLatin1StringView(e.text),
+ htmlMenu);
+ entityAction->setData(QLatin1StringView(e.entity));
+ htmlMenu->addAction(entityAction);
+ }
+
+ menu->addMenu(htmlMenu);
+ connect(htmlMenu, &QMenu::triggered, this, &HtmlTextEdit::actionTriggered);
+ menu->exec(event->globalPos());
+ delete menu;
+}
+
+void HtmlTextEdit::actionTriggered(QAction *action)
+{
+ insertPlainText(action->data().toString());
+}
diff --git a/src/richtexteditor.cpp b/src/richtexteditor.cpp
new file mode 100644
index 0000000..e43f2c1
--- /dev/null
+++ b/src/richtexteditor.cpp
@@ -0,0 +1,179 @@
+#include "richtexteditor.h"
+#include "richtexteditortoolbar.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+const bool simplifyRichTextDefault = true;
+
+using namespace Qt::StringLiterals;
+
+// Richtext simplification filter helpers: Elements to be discarded
+static inline bool filterElement(QStringView name)
+{
+ return name != "meta"_L1 && name != "style"_L1;
+}
+
+// Richtext simplification filter helpers: Filter attributes of elements
+static inline void filterAttributes(QStringView name,
+ QXmlStreamAttributes *atts,
+ bool *paragraphAlignmentFound)
+{
+ if (atts->isEmpty())
+ return;
+
+ // No style attributes for
+ if (name == "body"_L1) {
+ atts->clear();
+ return;
+ }
+
+ // Clean out everything except 'align' for 'p'
+ if (name == "p"_L1) {
+ for (auto it = atts->begin(); it != atts->end(); ) {
+ if (it->name() == "align"_L1) {
+ ++it;
+ *paragraphAlignmentFound = true;
+ } else {
+ it = atts->erase(it);
+ }
+ }
+ return;
+ }
+}
+
+// Richtext simplification filter helpers: Check for blank QStringView.
+static inline bool isWhiteSpace(QStringView in)
+{
+ return std::all_of(in.cbegin(), in.cend(),
+ [](QChar c) { return c.isSpace(); });
+}
+
+// Richtext simplification filter: Remove hard-coded font settings,
+//