initial upload

This commit is contained in:
2023-11-15 20:54:49 +01:00
parent b90a34c4b4
commit 99dad98ce0
29 changed files with 1277 additions and 0 deletions

17
iconos.qrc Normal file
View File

@@ -0,0 +1,17 @@
<RCC>
<qresource prefix="/">
<file>images/insertimage.png</file>
<file>images/simplifyrichtext.png</file>
<file>images/textanchor.png</file>
<file>images/textbold.png</file>
<file>images/textcenter.png</file>
<file>images/textitalic.png</file>
<file>images/textjustify.png</file>
<file>images/textleft.png</file>
<file>images/textright.png</file>
<file>images/textsubscript.png</file>
<file>images/textsuperscript.png</file>
<file>images/textunder.png</file>
<file>images/righttoleft.png</file>
</qresource>
</RCC>

BIN
images/insertimage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

BIN
images/righttoleft.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

BIN
images/simplifyrichtext.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
images/textanchor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
images/textbold.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
images/textcenter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

BIN
images/textitalic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

BIN
images/textjustify.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

BIN
images/textleft.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

BIN
images/textright.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

BIN
images/textsubscript.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

BIN
images/textsuperscript.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

BIN
images/textunder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

27
include/addlinkdialog.h Normal file
View File

@@ -0,0 +1,27 @@
#ifndef ADDLINKDIALOG_H
#define ADDLINKDIALOG_H
#include "ui_addlinkdialog.h"
#include "richtexteditor.h"
#include <QDialog>
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

26
include/coloraction.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef COLORACTION_H
#define COLORACTION_H
#include <QAction>
#include <QColor>
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

43
include/htmlhighlighter.h Normal file
View File

@@ -0,0 +1,43 @@
#ifndef HTMLHIGHLIGHTER_H
#define HTMLHIGHLIGHTER_H
#include <QSyntaxHighlighter>
#include <QTextCharFormat>
#include <QTextEdit>
/* 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

23
include/htmltextedit.h Normal file
View File

@@ -0,0 +1,23 @@
#ifndef HTMLTEXTEDIT_H
#define HTMLTEXTEDIT_H
#include <QTextEdit>
#include <QAction>
#include <QContextMenuEvent>
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

34
include/richtexteditor.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef RICHTEXTEDITOR_H
#define RICHTEXTEDITOR_H
#include <QTextEdit>
#include <QToolBar>
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

View File

@@ -0,0 +1,37 @@
#ifndef RICHTEXTEDITORDIALOG_H
#define RICHTEXTEDITORDIALOG_H
#include "richtexteditor.h"
#include <QDialog>
#include <QTextEdit>
#include <QTabWidget>
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

View File

@@ -0,0 +1,57 @@
#ifndef RICHTEXTEDITORTOOLBAR_H
#define RICHTEXTEDITORTOOLBAR_H
#include "richtexteditor.h"
#include "coloraction.h"
#include <QToolBar>
#include <QComboBox>
#include <QFontComboBox>
#include <QPointer>
#include <QAction>
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<RichTextEditor> m_editor;
};
#endif // RICHTEXTEDITORTOOLBAR_H

45
src/addlinkdialog.cpp Normal file
View File

@@ -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 = "<a href=\"" + url + "\">" + title + "</a>";
m_editor->insertHtml(html);
}
m_ui->titleInput->clear();
m_ui->urlInput->clear();
QDialog::accept();
}

36
src/coloraction.cpp Normal file
View File

@@ -0,0 +1,36 @@
#include "coloraction.h"
#include <QPainter>
#include <QPixmap>
#include <QColorDialog>
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);
}
}

142
src/htmlhighlighter.cpp Normal file
View File

@@ -0,0 +1,142 @@
#include "htmlhighlighter.h"
#include <QTextStream>
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)) {
state = InComment;
} else {
state = InTag;
start = pos;
while (pos < len && text.at(pos) != space
&& text.at(pos) != u'>'
&& text.at(pos) != tab
&& !QStringView{text}.sliced(pos).startsWith("/>"_L1)) {
++pos;
}
if (QStringView{text}.sliced(pos).startsWith("/>"_L1))
++pos;
setFormat(start, pos - start,
m_formats[Tag]);
break;
}
break;
}
if (ch == u'&') {
start = pos;
while (pos < len && text.at(pos++) != u';')
;
setFormat(start, pos - start,
m_formats[Entity]);
} else {
// No tag, comment or entity started, continue...
++pos;
}
}
break;
case InComment:
start = pos;
for ( ; pos < len; ++pos) {
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);
}

40
src/htmltextedit.cpp Normal file
View File

@@ -0,0 +1,40 @@
#include "htmltextedit.h"
#include <QMenu>
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[] = {
{ "&&amp; (&&)", "&amp;" },
{ "&&nbsp;", "&nbsp;" },
{ "&&lt; (<)", "&lt;" },
{ "&&gt; (>)", "&gt;" },
{ "&&copy; (Copyright)", "&copy;" },
{ "&&reg; (Trade Mark)", "&reg;" },
};
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());
}

179
src/richtexteditor.cpp Normal file
View File

@@ -0,0 +1,179 @@
#include "richtexteditor.h"
#include "richtexteditortoolbar.h"
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <QStringView>
#include <QStringLiteral>
#include <algorithm>
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 <body>
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,
// <style> elements, <p> attributes other than 'align' and
// and unnecessary meta-information.
QString simplifyRichTextFilter(const QString &in, bool *isPlainTextPtr = nullptr)
{
unsigned elementCount = 0;
bool paragraphAlignmentFound = false;
QString out;
QXmlStreamReader reader(in);
QXmlStreamWriter writer(&out);
writer.setAutoFormatting(false);
writer.setAutoFormattingIndent(0);
while (!reader.atEnd()) {
switch (reader.readNext()) {
case QXmlStreamReader::StartElement:
elementCount++;
if (filterElement(reader.name())) {
const auto name = reader.name();
QXmlStreamAttributes attributes = reader.attributes();
filterAttributes(name, &attributes, &paragraphAlignmentFound);
writer.writeStartElement(name.toString());
if (!attributes.isEmpty())
writer.writeAttributes(attributes);
} else {
reader.readElementText(); // Skip away all nested elements and characters.
}
break;
case QXmlStreamReader::Characters:
if (!isWhiteSpace(reader.text()))
writer.writeCharacters(reader.text().toString());
break;
case QXmlStreamReader::EndElement:
writer.writeEndElement();
break;
default:
break;
}
}
// Check for plain text (no spans, just <html><head><body><p>)
if (isPlainTextPtr)
*isPlainTextPtr = !paragraphAlignmentFound && elementCount == 4u; //
return out;
}
RichTextEditor::RichTextEditor(QWidget *parent)
: QTextEdit(parent), m_simplifyRichText(simplifyRichTextDefault)
{
connect(this, &RichTextEditor::currentCharFormatChanged,
this, &RichTextEditor::stateChanged);
connect(this, &RichTextEditor::cursorPositionChanged,
this, &RichTextEditor::stateChanged);
}
QToolBar *RichTextEditor::createToolBar(QWidget *parent)
{
return new RichTextEditorToolBar(this, parent);
}
void RichTextEditor::setFontBold(bool b)
{
if (b)
setFontWeight(QFont::Bold);
else
setFontWeight(QFont::Normal);
}
void RichTextEditor::setFontPointSize(double d)
{
QTextEdit::setFontPointSize(qreal(d));
}
void RichTextEditor::setText(const QString &text)
{
if (Qt::mightBeRichText(text))
setHtml(text);
else
setPlainText(text);
}
void RichTextEditor::setSimplifyRichText(bool v)
{
if (v != m_simplifyRichText) {
m_simplifyRichText = v;
emit simplifyRichTextChanged(v);
}
}
void RichTextEditor::setDefaultFont(QFont font)
{
// Some default fonts on Windows have a default size of 7.8,
// which results in complicated rich text generated by toHtml().
// Use an integer value.
const int pointSize = qRound(font.pointSizeF());
if (pointSize > 0 && !qFuzzyCompare(qreal(pointSize), font.pointSizeF())) {
font.setPointSize(pointSize);
}
document()->setDefaultFont(font);
if (font.pointSize() > 0)
setFontPointSize(font.pointSize());
else
setFontPointSize(QFontInfo(font).pointSize());
emit textChanged();
}
QString RichTextEditor::text(Qt::TextFormat format) const
{
switch (format) {
case Qt::PlainText:
return toPlainText();
case Qt::RichText:
return m_simplifyRichText ? simplifyRichTextFilter(toHtml()) : toHtml();
default:
break;
}
const QString html = toHtml();
bool isPlainText;
const QString simplifiedHtml = simplifyRichTextFilter(html, &isPlainText);
if (isPlainText)
return toPlainText();
return m_simplifyRichText ? simplifiedHtml : html;
}

View File

@@ -0,0 +1,143 @@
#include "richtexteditordialog.h"
#include "htmltextedit.h"
#include "htmlhighlighter.h"
#include <QVBoxLayout>
#include <QToolBar>
#include <QDialogButtonBox>
#include <QPushButton>
RichTextEditorDialog::RichTextEditorDialog(QWidget *parent) :
QDialog(parent),
m_editor(new RichTextEditor()),
m_text_edit(new HtmlTextEdit),
m_tab_widget(new QTabWidget),
m_state(Clean),
m_initialTab(RichTextIndex)
{
setWindowTitle(tr("Edit text"));
m_initialTab = RichTextIndex;
m_text_edit->setAcceptRichText(false);
new HtmlHighlighter(m_text_edit);
connect(m_editor, &QTextEdit::textChanged, this, &RichTextEditorDialog::richTextChanged);
connect(m_editor, &RichTextEditor::simplifyRichTextChanged,
this, &RichTextEditorDialog::richTextChanged);
connect(m_text_edit, &QTextEdit::textChanged, this, &RichTextEditorDialog::sourceChanged);
// The toolbar needs to be created after the RichTextEditor
QToolBar *tool_bar = m_editor->createToolBar(this);
tool_bar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
QWidget *rich_edit = new QWidget;
QVBoxLayout *rich_edit_layout = new QVBoxLayout(rich_edit);
rich_edit_layout->addWidget(tool_bar);
rich_edit_layout->addWidget(m_editor);
QWidget *plain_edit = new QWidget;
QVBoxLayout *plain_edit_layout = new QVBoxLayout(plain_edit);
plain_edit_layout->addWidget(m_text_edit);
m_tab_widget->setTabPosition(QTabWidget::South);
m_tab_widget->addTab(rich_edit, tr("Rich Text"));
m_tab_widget->addTab(plain_edit, tr("Source"));
connect(m_tab_widget, &QTabWidget::currentChanged,
this, &RichTextEditorDialog::tabIndexChanged);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal);
QPushButton *ok_button = buttonBox->button(QDialogButtonBox::Ok);
ok_button->setText(tr("&OK"));
ok_button->setDefault(true);
buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("&Cancel"));
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(m_tab_widget);
layout->addWidget(buttonBox);
}
RichTextEditorDialog::~RichTextEditorDialog()
{}
int RichTextEditorDialog::showDialog()
{
m_tab_widget->setCurrentIndex(m_initialTab);
switch (m_initialTab) {
case RichTextIndex:
m_editor->selectAll();
m_editor->setFocus();
break;
case SourceIndex:
m_text_edit->selectAll();
m_text_edit->setFocus();
break;
}
return exec();
}
void RichTextEditorDialog::setDefaultFont(const QFont &font)
{
m_editor->setDefaultFont(font);
}
void RichTextEditorDialog::setText(const QString &text)
{
// Generally simplify rich text unless verbose text is found.
const bool isSimplifiedRichText = !text.startsWith(QStringLiteral("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">"));
m_editor->setSimplifyRichText(isSimplifiedRichText);
m_editor->setText(text);
m_text_edit->setPlainText(text);
m_state = Clean;
}
QString RichTextEditorDialog::text(Qt::TextFormat format) const
{
// In autotext mode, if the user has changed the source, use that
if (format == Qt::AutoText && (m_state == Clean || m_state == SourceChanged))
return m_text_edit->toPlainText();
// If the plain text HTML editor is selected, first copy its contents over
// to the rich text editor so that it is converted to Qt-HTML or actual
// plain text.
if (m_tab_widget->currentIndex() == SourceIndex && m_state == SourceChanged)
m_editor->setHtml(m_text_edit->toPlainText());
return m_editor->text(format);
}
void RichTextEditorDialog::tabIndexChanged(int newIndex)
{
// Anything changed, is there a need for a conversion?
if (newIndex == SourceIndex && m_state != RichTextChanged)
return;
if (newIndex == RichTextIndex && m_state != SourceChanged)
return;
const State oldState = m_state;
// Remember the cursor position, since it is invalidated by setPlainText
QTextEdit *new_edit = (newIndex == SourceIndex) ? m_text_edit : m_editor;
const int position = new_edit->textCursor().position();
if (newIndex == SourceIndex)
m_text_edit->setPlainText(m_editor->text(Qt::RichText));
else
m_editor->setHtml(m_text_edit->toPlainText());
QTextCursor cursor = new_edit->textCursor();
cursor.movePosition(QTextCursor::End);
if (cursor.position() > position) {
cursor.setPosition(position);
}
new_edit->setTextCursor(cursor);
m_state = oldState; // Changed is triggered by setting the text
}
void RichTextEditorDialog::richTextChanged()
{
m_state = RichTextChanged;
}
void RichTextEditorDialog::sourceChanged()
{
m_state = SourceChanged;
}

View File

@@ -0,0 +1,316 @@
#include "richtexteditortoolbar.h"
#include "addlinkdialog.h"
#include <QFontDatabase>
#include <QActionGroup>
#include <QTextBlock>
#include <QTextBlockFormat>
#include <QTextCursor>
#include <QFileDialog>
#include <QFile>
#include <QDir>
using namespace Qt::StringLiterals;
QIcon createIconSet(const QString &name)
{
constexpr QLatin1StringView prefixes[] = {
":/images/"_L1,
};
for (QLatin1StringView prefix : prefixes) {
const QString f = prefix + name;
if (QFile::exists(f))
return QIcon(f);
}
return QIcon();
}
static QAction *createCheckableAction(const QIcon &icon, const QString &text,
QObject *parent = nullptr)
{
QAction *result = new QAction(parent);
result->setIcon(icon);
result->setText(text);
result->setCheckable(true);
result->setChecked(false);
return result;
}
RichTextEditorToolBar::RichTextEditorToolBar(RichTextEditor *editor,
QWidget *parent) :
QToolBar(parent),
m_link_action(new QAction(this)),
m_image_action(new QAction(this)),
m_color_action(new ColorAction(this)),
m_font_size_input(new QComboBox),
m_editor(editor)
{
// Font comboBox
m_font_family_input = new QFontComboBox(this);
m_font_family_input->setEditable(false);
connect(m_font_family_input, &QComboBox::textActivated,
this, &RichTextEditorToolBar::fontFamilyActivated);
addWidget(m_font_family_input);
// Font size combo box
m_font_size_input->setEditable(false);
const auto font_sizes = QFontDatabase::standardSizes();
for (int font_size : font_sizes)
m_font_size_input->addItem(QString::number(font_size));
connect(m_font_size_input, &QComboBox::textActivated,
this, &RichTextEditorToolBar::sizeInputActivated);
addWidget(m_font_size_input);
addSeparator();
// Bold, italic and underline buttons
m_bold_action = createCheckableAction(
createIconSet(u"textbold.png"_s), tr("Bold"), this);
connect(m_bold_action, &QAction::triggered, editor, &RichTextEditor::setFontBold);
m_bold_action->setShortcut(tr("CTRL+B"));
addAction(m_bold_action);
m_italic_action = createCheckableAction(
createIconSet(u"textitalic.png"_s), tr("Italic"), this);
connect(m_italic_action, &QAction::triggered, editor, &RichTextEditor::setFontItalic);
m_italic_action->setShortcut(tr("CTRL+I"));
addAction(m_italic_action);
m_underline_action = createCheckableAction(
createIconSet(u"textunder.png"_s), tr("Underline"), this);
connect(m_underline_action, &QAction::triggered, editor, &RichTextEditor::setFontUnderline);
m_underline_action->setShortcut(tr("CTRL+U"));
addAction(m_underline_action);
addSeparator();
// Left, center, right and justified alignment buttons
QActionGroup *alignment_group = new QActionGroup(this);
connect(alignment_group, &QActionGroup::triggered,
this, &RichTextEditorToolBar::alignmentActionTriggered);
m_align_left_action = createCheckableAction(
createIconSet(u"textleft.png"_s), tr("Left Align"), alignment_group);
addAction(m_align_left_action);
m_align_center_action = createCheckableAction(
createIconSet(u"textcenter.png"_s), tr("Center"), alignment_group);
addAction(m_align_center_action);
m_align_right_action = createCheckableAction(
createIconSet(u"textright.png"_s), tr("Right Align"), alignment_group);
addAction(m_align_right_action);
m_align_justify_action = createCheckableAction(
createIconSet(u"textjustify.png"_s), tr("Justify"), alignment_group);
addAction(m_align_justify_action);
m_layoutDirectionAction = createCheckableAction(
createIconSet(u"righttoleft.png"_s), tr("Right to Left"));
connect(m_layoutDirectionAction, &QAction::triggered,
this, &RichTextEditorToolBar::layoutDirectionChanged);
addAction(m_layoutDirectionAction);
addSeparator();
// Superscript and subscript buttons
m_valign_sup_action = createCheckableAction(
createIconSet(u"textsuperscript.png"_s), tr("Superscript"), this);
connect(m_valign_sup_action, &QAction::triggered,
this, &RichTextEditorToolBar::setVAlignSuper);
addAction(m_valign_sup_action);
m_valign_sub_action = createCheckableAction(
createIconSet(u"textsubscript.png"_s), tr("Subscript"), this);
connect(m_valign_sub_action, &QAction::triggered,
this, &RichTextEditorToolBar::setVAlignSub);
addAction(m_valign_sub_action);
addSeparator();
// Insert hyperlink and image buttons
m_link_action->setIcon(createIconSet(u"textanchor.png"_s));
m_link_action->setText(tr("Insert &Link"));
connect(m_link_action, &QAction::triggered, this, &RichTextEditorToolBar::insertLink);
addAction(m_link_action);
m_image_action->setIcon(createIconSet(u"insertimage.png"_s));
m_image_action->setText(tr("Insert &Image"));
connect(m_image_action, &QAction::triggered, this, &RichTextEditorToolBar::insertImage);
addAction(m_image_action);
addSeparator();
// Text color button
connect(m_color_action, &ColorAction::colorChanged,
this, &RichTextEditorToolBar::colorChanged);
addAction(m_color_action);
addSeparator();
// Simplify rich text
m_simplify_richtext_action
= createCheckableAction(createIconSet(u"simplifyrichtext.png"_s), tr("Simplify Rich Text"));
connect(m_simplify_richtext_action, &QAction::triggered,
m_editor, &RichTextEditor::setSimplifyRichText);
m_simplify_richtext_action->setChecked(m_editor->simplifyRichText());
connect(m_editor.data(), &RichTextEditor::simplifyRichTextChanged,
m_simplify_richtext_action, &QAction::setChecked);
addAction(m_simplify_richtext_action);
connect(editor, &QTextEdit::textChanged, this, &RichTextEditorToolBar::updateActions);
connect(editor, &RichTextEditor::stateChanged, this, &RichTextEditorToolBar::updateActions);
updateActions();
}
void RichTextEditorToolBar::alignmentActionTriggered(QAction *action)
{
Qt::Alignment new_alignment;
if (action == m_align_left_action) {
new_alignment = Qt::AlignLeft;
} else if (action == m_align_center_action) {
new_alignment = Qt::AlignCenter;
} else if (action == m_align_right_action) {
new_alignment = Qt::AlignRight;
} else {
new_alignment = Qt::AlignJustify;
}
m_editor->setAlignment(new_alignment);
}
void RichTextEditorToolBar::colorChanged(const QColor &color)
{
m_editor->setTextColor(color);
m_editor->setFocus();
}
void RichTextEditorToolBar::sizeInputActivated(const QString &size)
{
bool ok;
int i = size.toInt(&ok);
if (!ok)
return;
m_editor->setFontPointSize(i);
m_editor->setFocus();
}
void RichTextEditorToolBar::fontFamilyActivated(const QString &font)
{
QTextCharFormat fmt;
fmt.setFontFamilies({font});
QTextCursor cursor = m_editor->textCursor();
if (!cursor.hasSelection())
cursor.select(QTextCursor::WordUnderCursor);
cursor.mergeCharFormat(fmt);
m_editor->mergeCurrentCharFormat(fmt);
}
void RichTextEditorToolBar::setVAlignSuper(bool super)
{
const QTextCharFormat::VerticalAlignment align = super ?
QTextCharFormat::AlignSuperScript : QTextCharFormat::AlignNormal;
QTextCharFormat charFormat = m_editor->currentCharFormat();
charFormat.setVerticalAlignment(align);
m_editor->setCurrentCharFormat(charFormat);
m_valign_sub_action->setChecked(false);
}
void RichTextEditorToolBar::setVAlignSub(bool sub)
{
const QTextCharFormat::VerticalAlignment align = sub ?
QTextCharFormat::AlignSubScript : QTextCharFormat::AlignNormal;
QTextCharFormat charFormat = m_editor->currentCharFormat();
charFormat.setVerticalAlignment(align);
m_editor->setCurrentCharFormat(charFormat);
m_valign_sup_action->setChecked(false);
}
void RichTextEditorToolBar::insertLink()
{
AddLinkDialog linkDialog(m_editor, this);
linkDialog.showDialog();
m_editor->setFocus();
}
void RichTextEditorToolBar::insertImage()
{
const QString path = QFileDialog::getOpenFileName(this, tr("Open File"),
QDir::homePath(),
tr("Images (*.png *.xpm *.jpg)"));
if (!path.isEmpty())
m_editor->insertHtml(QStringLiteral("<img src=\"") + path + QStringLiteral("\"/>"));
}
void RichTextEditorToolBar::layoutDirectionChanged()
{
QTextCursor cursor = m_editor->textCursor();
QTextBlock block = cursor.block();
if (block.isValid()) {
QTextBlockFormat format = block.blockFormat();
const Qt::LayoutDirection newDirection = m_layoutDirectionAction->isChecked() ? Qt::RightToLeft : Qt::LeftToRight;
if (format.layoutDirection() != newDirection) {
format.setLayoutDirection(newDirection);
cursor.setBlockFormat(format);
}
}
}
void RichTextEditorToolBar::updateActions()
{
if (m_editor == nullptr) {
setEnabled(false);
return;
}
const Qt::Alignment alignment = m_editor->alignment();
const QTextCursor cursor = m_editor->textCursor();
const QTextCharFormat charFormat = cursor.charFormat();
const QFont font = charFormat.font();
const QTextCharFormat::VerticalAlignment valign =
charFormat.verticalAlignment();
const bool superScript = valign == QTextCharFormat::AlignSuperScript;
const bool subScript = valign == QTextCharFormat::AlignSubScript;
if (alignment & Qt::AlignLeft) {
m_align_left_action->setChecked(true);
} else if (alignment & Qt::AlignRight) {
m_align_right_action->setChecked(true);
} else if (alignment & Qt::AlignHCenter) {
m_align_center_action->setChecked(true);
} else {
m_align_justify_action->setChecked(true);
}
m_layoutDirectionAction->setChecked(cursor.blockFormat().layoutDirection() == Qt::RightToLeft);
m_bold_action->setChecked(font.bold());
m_italic_action->setChecked(font.italic());
m_underline_action->setChecked(font.underline());
m_valign_sup_action->setChecked(superScript);
m_valign_sub_action->setChecked(subScript);
const int size = font.pointSize();
const int idx = m_font_size_input->findText(QString::number(size));
if (idx != -1)
m_font_size_input->setCurrentIndex(idx);
const int fidx = m_font_family_input->findText(QFontInfo(font).family());
if (fidx != -1)
m_font_family_input->setCurrentIndex(fidx);
m_color_action->setColor(m_editor->textColor());
}

112
uis/addlinkdialog.ui Normal file
View File

@@ -0,0 +1,112 @@
<ui version="4.0" >
<class>AddLinkDialog</class>
<widget class="QDialog" name="AddLinkDialog" >
<property name="windowTitle" >
<string>Insert Link</string>
</property>
<property name="sizeGripEnabled" >
<bool>false</bool>
</property>
<property name="modal" >
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout" >
<item>
<layout class="QFormLayout" >
<item row="0" column="0" >
<widget class="QLabel" name="label" >
<property name="text" >
<string>Title:</string>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="QLineEdit" name="titleInput" >
<property name="minimumSize" >
<size>
<width>337</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_2" >
<property name="text" >
<string>URL:</string>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QLineEdit" name="urlInput" />
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer" >
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddLinkDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel" >
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddLinkDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel" >
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>