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, +//