1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the Qt Designer of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include "richtexteditor_p.h" |
30 | #include "htmlhighlighter_p.h" |
31 | #include "iconselector_p.h" |
32 | #include "ui_addlinkdialog.h" |
33 | |
34 | #include "iconloader_p.h" |
35 | |
36 | #include <QtDesigner/abstractformeditor.h> |
37 | #include <QtDesigner/abstractsettings.h> |
38 | |
39 | #include <QtCore/qlist.h> |
40 | #include <QtCore/qmap.h> |
41 | #include <QtCore/qpointer.h> |
42 | #include <QtCore/qxmlstream.h> |
43 | |
44 | #include <QtWidgets/qaction.h> |
45 | #include <QtWidgets/qcolordialog.h> |
46 | #include <QtWidgets/qcombobox.h> |
47 | #include <QtGui/qfontdatabase.h> |
48 | #include <QtGui/qtextcursor.h> |
49 | #include <QtGui/qpainter.h> |
50 | #include <QtGui/qicon.h> |
51 | #include <QtWidgets/qmenu.h> |
52 | #include <QtGui/qevent.h> |
53 | #include <QtWidgets/qtabwidget.h> |
54 | #include <QtGui/qtextobject.h> |
55 | #include <QtGui/qtextdocument.h> |
56 | #include <QtWidgets/qtoolbar.h> |
57 | #include <QtWidgets/qtoolbutton.h> |
58 | #include <QtWidgets/qboxlayout.h> |
59 | #include <QtWidgets/qpushbutton.h> |
60 | #include <QtWidgets/qdialogbuttonbox.h> |
61 | |
62 | QT_BEGIN_NAMESPACE |
63 | |
64 | static const char RichTextDialogGroupC[] = "RichTextDialog" ; |
65 | static const char GeometryKeyC[] = "Geometry" ; |
66 | static const char TabKeyC[] = "Tab" ; |
67 | |
68 | const bool simplifyRichTextDefault = true; |
69 | |
70 | namespace qdesigner_internal { |
71 | |
72 | // Richtext simplification filter helpers: Elements to be discarded |
73 | static inline bool filterElement(const QStringRef &name) |
74 | { |
75 | return name != QStringLiteral("meta" ) && name != QStringLiteral("style" ); |
76 | } |
77 | |
78 | // Richtext simplification filter helpers: Filter attributes of elements |
79 | static inline void filterAttributes(const QStringRef &name, |
80 | QXmlStreamAttributes *atts, |
81 | bool *paragraphAlignmentFound) |
82 | { |
83 | if (atts->isEmpty()) |
84 | return; |
85 | |
86 | // No style attributes for <body> |
87 | if (name == QStringLiteral("body" )) { |
88 | atts->clear(); |
89 | return; |
90 | } |
91 | |
92 | // Clean out everything except 'align' for 'p' |
93 | if (name == QStringLiteral("p" )) { |
94 | for (auto it = atts->begin(); it != atts->end(); ) { |
95 | if (it->name() == QStringLiteral("align" )) { |
96 | ++it; |
97 | *paragraphAlignmentFound = true; |
98 | } else { |
99 | it = atts->erase(pos: it); |
100 | } |
101 | } |
102 | return; |
103 | } |
104 | } |
105 | |
106 | // Richtext simplification filter helpers: Check for blank QStringRef. |
107 | static inline bool isWhiteSpace(const QStringRef &in) |
108 | { |
109 | const int count = in.size(); |
110 | for (int i = 0; i < count; i++) |
111 | if (!in.at(i).isSpace()) |
112 | return false; |
113 | return true; |
114 | } |
115 | |
116 | // Richtext simplification filter: Remove hard-coded font settings, |
117 | // <style> elements, <p> attributes other than 'align' and |
118 | // and unnecessary meta-information. |
119 | QString simplifyRichTextFilter(const QString &in, bool *isPlainTextPtr = nullptr) |
120 | { |
121 | unsigned elementCount = 0; |
122 | bool paragraphAlignmentFound = false; |
123 | QString out; |
124 | QXmlStreamReader reader(in); |
125 | QXmlStreamWriter writer(&out); |
126 | writer.setAutoFormatting(false); |
127 | writer.setAutoFormattingIndent(0); |
128 | |
129 | while (!reader.atEnd()) { |
130 | switch (reader.readNext()) { |
131 | case QXmlStreamReader::StartElement: |
132 | elementCount++; |
133 | if (filterElement(name: reader.name())) { |
134 | const auto name = reader.name(); |
135 | QXmlStreamAttributes attributes = reader.attributes(); |
136 | filterAttributes(name, atts: &attributes, paragraphAlignmentFound: ¶graphAlignmentFound); |
137 | writer.writeStartElement(qualifiedName: name.toString()); |
138 | if (!attributes.isEmpty()) |
139 | writer.writeAttributes(attributes); |
140 | } else { |
141 | reader.readElementText(); // Skip away all nested elements and characters. |
142 | } |
143 | break; |
144 | case QXmlStreamReader::Characters: |
145 | if (!isWhiteSpace(in: reader.text())) |
146 | writer.writeCharacters(text: reader.text().toString()); |
147 | break; |
148 | case QXmlStreamReader::EndElement: |
149 | writer.writeEndElement(); |
150 | break; |
151 | default: |
152 | break; |
153 | } |
154 | } |
155 | // Check for plain text (no spans, just <html><head><body><p>) |
156 | if (isPlainTextPtr) |
157 | *isPlainTextPtr = !paragraphAlignmentFound && elementCount == 4u; // |
158 | return out; |
159 | } |
160 | |
161 | class RichTextEditor : public QTextEdit |
162 | { |
163 | Q_OBJECT |
164 | public: |
165 | explicit RichTextEditor(QWidget *parent = nullptr); |
166 | void setDefaultFont(QFont font); |
167 | |
168 | QToolBar *createToolBar(QDesignerFormEditorInterface *core, QWidget *parent = nullptr); |
169 | |
170 | bool simplifyRichText() const { return m_simplifyRichText; } |
171 | |
172 | public slots: |
173 | void setFontBold(bool b); |
174 | void setFontPointSize(double); |
175 | void setText(const QString &text); |
176 | void setSimplifyRichText(bool v); |
177 | QString text(Qt::TextFormat format) const; |
178 | |
179 | signals: |
180 | void stateChanged(); |
181 | void simplifyRichTextChanged(bool); |
182 | |
183 | private: |
184 | bool m_simplifyRichText; |
185 | }; |
186 | |
187 | class AddLinkDialog : public QDialog |
188 | { |
189 | Q_OBJECT |
190 | |
191 | public: |
192 | AddLinkDialog(RichTextEditor *editor, QWidget *parent = nullptr); |
193 | ~AddLinkDialog() override; |
194 | |
195 | int showDialog(); |
196 | |
197 | public slots: |
198 | void accept() override; |
199 | |
200 | private: |
201 | RichTextEditor *m_editor; |
202 | Ui::AddLinkDialog *m_ui; |
203 | }; |
204 | |
205 | AddLinkDialog::AddLinkDialog(RichTextEditor *editor, QWidget *parent) : |
206 | QDialog(parent), |
207 | m_ui(new Ui::AddLinkDialog) |
208 | { |
209 | m_ui->setupUi(this); |
210 | |
211 | setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); |
212 | |
213 | m_editor = editor; |
214 | } |
215 | |
216 | AddLinkDialog::~AddLinkDialog() |
217 | { |
218 | delete m_ui; |
219 | } |
220 | |
221 | int AddLinkDialog::showDialog() |
222 | { |
223 | // Set initial focus |
224 | const QTextCursor cursor = m_editor->textCursor(); |
225 | if (cursor.hasSelection()) { |
226 | m_ui->titleInput->setText(cursor.selectedText()); |
227 | m_ui->urlInput->setFocus(); |
228 | } else { |
229 | m_ui->titleInput->setFocus(); |
230 | } |
231 | |
232 | return exec(); |
233 | } |
234 | |
235 | void AddLinkDialog::accept() |
236 | { |
237 | const QString title = m_ui->titleInput->text(); |
238 | const QString url = m_ui->urlInput->text(); |
239 | |
240 | if (!title.isEmpty()) { |
241 | QString html = QStringLiteral("<a href=\"" ); |
242 | html += url; |
243 | html += QStringLiteral("\">" ); |
244 | html += title; |
245 | html += QStringLiteral("</a>" ); |
246 | |
247 | m_editor->insertHtml(text: html); |
248 | } |
249 | |
250 | m_ui->titleInput->clear(); |
251 | m_ui->urlInput->clear(); |
252 | |
253 | QDialog::accept(); |
254 | } |
255 | |
256 | class HtmlTextEdit : public QTextEdit |
257 | { |
258 | Q_OBJECT |
259 | |
260 | public: |
261 | HtmlTextEdit(QWidget *parent = nullptr) |
262 | : QTextEdit(parent) |
263 | {} |
264 | |
265 | void contextMenuEvent(QContextMenuEvent *event) override; |
266 | |
267 | private slots: |
268 | void actionTriggered(QAction *action); |
269 | }; |
270 | |
271 | void HtmlTextEdit::(QContextMenuEvent *event) |
272 | { |
273 | QMenu * = createStandardContextMenu(); |
274 | QMenu * = new QMenu(tr(s: "Insert HTML entity" ), menu); |
275 | |
276 | typedef struct { |
277 | const char *text; |
278 | const char *entity; |
279 | } Entry; |
280 | |
281 | const Entry entries[] = { |
282 | { .text: "&& (&&)" , .entity: "&" }, |
283 | { .text: "& " , .entity: " " }, |
284 | { .text: "&< (<)" , .entity: "<" }, |
285 | { .text: "&> (>)" , .entity: ">" }, |
286 | { .text: "&© (Copyright)" , .entity: "©" }, |
287 | { .text: "&® (Trade Mark)" , .entity: "®" }, |
288 | }; |
289 | |
290 | for (const Entry &e : entries) { |
291 | QAction *entityAction = new QAction(QLatin1String(e.text), |
292 | htmlMenu); |
293 | entityAction->setData(QLatin1String(e.entity)); |
294 | htmlMenu->addAction(action: entityAction); |
295 | } |
296 | |
297 | menu->addMenu(menu: htmlMenu); |
298 | connect(sender: htmlMenu, signal: &QMenu::triggered, receiver: this, slot: &HtmlTextEdit::actionTriggered); |
299 | menu->exec(pos: event->globalPos()); |
300 | delete menu; |
301 | } |
302 | |
303 | void HtmlTextEdit::actionTriggered(QAction *action) |
304 | { |
305 | insertPlainText(text: action->data().toString()); |
306 | } |
307 | |
308 | class ColorAction : public QAction |
309 | { |
310 | Q_OBJECT |
311 | |
312 | public: |
313 | ColorAction(QObject *parent); |
314 | |
315 | const QColor& color() const { return m_color; } |
316 | void setColor(const QColor &color); |
317 | |
318 | signals: |
319 | void colorChanged(const QColor &color); |
320 | |
321 | private slots: |
322 | void chooseColor(); |
323 | |
324 | private: |
325 | QColor m_color; |
326 | }; |
327 | |
328 | ColorAction::ColorAction(QObject *parent): |
329 | QAction(parent) |
330 | { |
331 | setText(tr(s: "Text Color" )); |
332 | setColor(Qt::black); |
333 | connect(sender: this, signal: &QAction::triggered, receiver: this, slot: &ColorAction::chooseColor); |
334 | } |
335 | |
336 | void ColorAction::setColor(const QColor &color) |
337 | { |
338 | if (color == m_color) |
339 | return; |
340 | m_color = color; |
341 | QPixmap pix(24, 24); |
342 | QPainter painter(&pix); |
343 | painter.setRenderHint(hint: QPainter::Antialiasing, on: false); |
344 | painter.fillRect(pix.rect(), color: m_color); |
345 | painter.setPen(m_color.darker()); |
346 | painter.drawRect(r: pix.rect().adjusted(xp1: 0, yp1: 0, xp2: -1, yp2: -1)); |
347 | setIcon(pix); |
348 | } |
349 | |
350 | void ColorAction::chooseColor() |
351 | { |
352 | const QColor col = QColorDialog::getColor(initial: m_color, parent: nullptr); |
353 | if (col.isValid() && col != m_color) { |
354 | setColor(col); |
355 | emit colorChanged(color: m_color); |
356 | } |
357 | } |
358 | |
359 | class RichTextEditorToolBar : public QToolBar |
360 | { |
361 | Q_OBJECT |
362 | public: |
363 | RichTextEditorToolBar(QDesignerFormEditorInterface *core, |
364 | RichTextEditor *editor, |
365 | QWidget *parent = nullptr); |
366 | |
367 | public slots: |
368 | void updateActions(); |
369 | |
370 | private slots: |
371 | void alignmentActionTriggered(QAction *action); |
372 | void sizeInputActivated(const QString &size); |
373 | void colorChanged(const QColor &color); |
374 | void setVAlignSuper(bool super); |
375 | void setVAlignSub(bool sub); |
376 | void insertLink(); |
377 | void insertImage(); |
378 | void layoutDirectionChanged(); |
379 | |
380 | private: |
381 | QAction *m_bold_action; |
382 | QAction *m_italic_action; |
383 | QAction *m_underline_action; |
384 | QAction *m_valign_sup_action; |
385 | QAction *m_valign_sub_action; |
386 | QAction *m_align_left_action; |
387 | QAction *m_align_center_action; |
388 | QAction *m_align_right_action; |
389 | QAction *m_align_justify_action; |
390 | QAction *m_layoutDirectionAction; |
391 | QAction *m_link_action; |
392 | QAction *m_image_action; |
393 | QAction *m_simplify_richtext_action; |
394 | ColorAction *m_color_action; |
395 | QComboBox *m_font_size_input; |
396 | |
397 | QDesignerFormEditorInterface *m_core; |
398 | QPointer<RichTextEditor> m_editor; |
399 | }; |
400 | |
401 | static QAction *createCheckableAction(const QIcon &icon, const QString &text, |
402 | QObject *receiver, const char *slot, |
403 | QObject *parent = nullptr) |
404 | { |
405 | QAction *result = new QAction(parent); |
406 | result->setIcon(icon); |
407 | result->setText(text); |
408 | result->setCheckable(true); |
409 | result->setChecked(false); |
410 | if (slot) |
411 | QObject::connect(sender: result, SIGNAL(triggered(bool)), receiver, member: slot); |
412 | return result; |
413 | } |
414 | |
415 | RichTextEditorToolBar::RichTextEditorToolBar(QDesignerFormEditorInterface *core, |
416 | RichTextEditor *editor, |
417 | QWidget *parent) : |
418 | QToolBar(parent), |
419 | m_link_action(new QAction(this)), |
420 | m_image_action(new QAction(this)), |
421 | m_color_action(new ColorAction(this)), |
422 | m_font_size_input(new QComboBox), |
423 | m_core(core), |
424 | m_editor(editor) |
425 | { |
426 | // Font size combo box |
427 | m_font_size_input->setEditable(false); |
428 | const auto font_sizes = QFontDatabase::standardSizes(); |
429 | for (int font_size : font_sizes) |
430 | m_font_size_input->addItem(atext: QString::number(font_size)); |
431 | |
432 | connect(sender: m_font_size_input, signal: &QComboBox::textActivated, |
433 | receiver: this, slot: &RichTextEditorToolBar::sizeInputActivated); |
434 | addWidget(widget: m_font_size_input); |
435 | |
436 | addSeparator(); |
437 | |
438 | // Bold, italic and underline buttons |
439 | |
440 | m_bold_action = createCheckableAction( |
441 | icon: createIconSet(QStringLiteral("textbold.png" )), |
442 | text: tr(s: "Bold" ), receiver: editor, SLOT(setFontBold(bool)), parent: this); |
443 | m_bold_action->setShortcut(tr(s: "CTRL+B" )); |
444 | addAction(action: m_bold_action); |
445 | |
446 | m_italic_action = createCheckableAction( |
447 | icon: createIconSet(QStringLiteral("textitalic.png" )), |
448 | text: tr(s: "Italic" ), receiver: editor, SLOT(setFontItalic(bool)), parent: this); |
449 | m_italic_action->setShortcut(tr(s: "CTRL+I" )); |
450 | addAction(action: m_italic_action); |
451 | |
452 | m_underline_action = createCheckableAction( |
453 | icon: createIconSet(QStringLiteral("textunder.png" )), |
454 | text: tr(s: "Underline" ), receiver: editor, SLOT(setFontUnderline(bool)), parent: this); |
455 | m_underline_action->setShortcut(tr(s: "CTRL+U" )); |
456 | addAction(action: m_underline_action); |
457 | |
458 | addSeparator(); |
459 | |
460 | // Left, center, right and justified alignment buttons |
461 | |
462 | QActionGroup *alignment_group = new QActionGroup(this); |
463 | connect(sender: alignment_group, signal: &QActionGroup::triggered, |
464 | receiver: this, slot: &RichTextEditorToolBar::alignmentActionTriggered); |
465 | |
466 | m_align_left_action = createCheckableAction( |
467 | icon: createIconSet(QStringLiteral("textleft.png" )), |
468 | text: tr(s: "Left Align" ), receiver: editor, slot: nullptr, parent: alignment_group); |
469 | addAction(action: m_align_left_action); |
470 | |
471 | m_align_center_action = createCheckableAction( |
472 | icon: createIconSet(QStringLiteral("textcenter.png" )), |
473 | text: tr(s: "Center" ), receiver: editor, slot: nullptr, parent: alignment_group); |
474 | addAction(action: m_align_center_action); |
475 | |
476 | m_align_right_action = createCheckableAction( |
477 | icon: createIconSet(QStringLiteral("textright.png" )), |
478 | text: tr(s: "Right Align" ), receiver: editor, slot: nullptr, parent: alignment_group); |
479 | addAction(action: m_align_right_action); |
480 | |
481 | m_align_justify_action = createCheckableAction( |
482 | icon: createIconSet(QStringLiteral("textjustify.png" )), |
483 | text: tr(s: "Justify" ), receiver: editor, slot: nullptr, parent: alignment_group); |
484 | addAction(action: m_align_justify_action); |
485 | |
486 | m_layoutDirectionAction = createCheckableAction( |
487 | icon: createIconSet(QStringLiteral("righttoleft.png" )), |
488 | text: tr(s: "Right to Left" ), receiver: this, SLOT(layoutDirectionChanged())); |
489 | addAction(action: m_layoutDirectionAction); |
490 | |
491 | addSeparator(); |
492 | |
493 | // Superscript and subscript buttons |
494 | |
495 | m_valign_sup_action = createCheckableAction( |
496 | icon: createIconSet(QStringLiteral("textsuperscript.png" )), |
497 | text: tr(s: "Superscript" ), |
498 | receiver: this, SLOT(setVAlignSuper(bool)), parent: this); |
499 | addAction(action: m_valign_sup_action); |
500 | |
501 | m_valign_sub_action = createCheckableAction( |
502 | icon: createIconSet(QStringLiteral("textsubscript.png" )), |
503 | text: tr(s: "Subscript" ), |
504 | receiver: this, SLOT(setVAlignSub(bool)), parent: this); |
505 | addAction(action: m_valign_sub_action); |
506 | |
507 | addSeparator(); |
508 | |
509 | // Insert hyperlink and image buttons |
510 | |
511 | m_link_action->setIcon(createIconSet(QStringLiteral("textanchor.png" ))); |
512 | m_link_action->setText(tr(s: "Insert &Link" )); |
513 | connect(sender: m_link_action, signal: &QAction::triggered, receiver: this, slot: &RichTextEditorToolBar::insertLink); |
514 | addAction(action: m_link_action); |
515 | |
516 | m_image_action->setIcon(createIconSet(QStringLiteral("insertimage.png" ))); |
517 | m_image_action->setText(tr(s: "Insert &Image" )); |
518 | connect(sender: m_image_action, signal: &QAction::triggered, receiver: this, slot: &RichTextEditorToolBar::insertImage); |
519 | addAction(action: m_image_action); |
520 | |
521 | addSeparator(); |
522 | |
523 | // Text color button |
524 | connect(sender: m_color_action, signal: &ColorAction::colorChanged, |
525 | receiver: this, slot: &RichTextEditorToolBar::colorChanged); |
526 | addAction(action: m_color_action); |
527 | |
528 | addSeparator(); |
529 | |
530 | // Simplify rich text |
531 | m_simplify_richtext_action |
532 | = createCheckableAction(icon: createIconSet(QStringLiteral("simplifyrichtext.png" )), |
533 | text: tr(s: "Simplify Rich Text" ), receiver: m_editor, SLOT(setSimplifyRichText(bool))); |
534 | m_simplify_richtext_action->setChecked(m_editor->simplifyRichText()); |
535 | connect(sender: m_editor.data(), signal: &RichTextEditor::simplifyRichTextChanged, |
536 | receiver: m_simplify_richtext_action, slot: &QAction::setChecked); |
537 | addAction(action: m_simplify_richtext_action); |
538 | |
539 | connect(sender: editor, signal: &QTextEdit::textChanged, receiver: this, slot: &RichTextEditorToolBar::updateActions); |
540 | connect(sender: editor, signal: &RichTextEditor::stateChanged, receiver: this, slot: &RichTextEditorToolBar::updateActions); |
541 | |
542 | updateActions(); |
543 | } |
544 | |
545 | void RichTextEditorToolBar::alignmentActionTriggered(QAction *action) |
546 | { |
547 | Qt::Alignment new_alignment; |
548 | |
549 | if (action == m_align_left_action) { |
550 | new_alignment = Qt::AlignLeft; |
551 | } else if (action == m_align_center_action) { |
552 | new_alignment = Qt::AlignCenter; |
553 | } else if (action == m_align_right_action) { |
554 | new_alignment = Qt::AlignRight; |
555 | } else { |
556 | new_alignment = Qt::AlignJustify; |
557 | } |
558 | |
559 | m_editor->setAlignment(new_alignment); |
560 | } |
561 | |
562 | void RichTextEditorToolBar::colorChanged(const QColor &color) |
563 | { |
564 | m_editor->setTextColor(color); |
565 | m_editor->setFocus(); |
566 | } |
567 | |
568 | void RichTextEditorToolBar::sizeInputActivated(const QString &size) |
569 | { |
570 | bool ok; |
571 | int i = size.toInt(ok: &ok); |
572 | if (!ok) |
573 | return; |
574 | |
575 | m_editor->setFontPointSize(i); |
576 | m_editor->setFocus(); |
577 | } |
578 | |
579 | void RichTextEditorToolBar::setVAlignSuper(bool super) |
580 | { |
581 | const QTextCharFormat::VerticalAlignment align = super ? |
582 | QTextCharFormat::AlignSuperScript : QTextCharFormat::AlignNormal; |
583 | |
584 | QTextCharFormat charFormat = m_editor->currentCharFormat(); |
585 | charFormat.setVerticalAlignment(align); |
586 | m_editor->setCurrentCharFormat(charFormat); |
587 | |
588 | m_valign_sub_action->setChecked(false); |
589 | } |
590 | |
591 | void RichTextEditorToolBar::setVAlignSub(bool sub) |
592 | { |
593 | const QTextCharFormat::VerticalAlignment align = sub ? |
594 | QTextCharFormat::AlignSubScript : QTextCharFormat::AlignNormal; |
595 | |
596 | QTextCharFormat charFormat = m_editor->currentCharFormat(); |
597 | charFormat.setVerticalAlignment(align); |
598 | m_editor->setCurrentCharFormat(charFormat); |
599 | |
600 | m_valign_sup_action->setChecked(false); |
601 | } |
602 | |
603 | void RichTextEditorToolBar::insertLink() |
604 | { |
605 | AddLinkDialog linkDialog(m_editor, this); |
606 | linkDialog.showDialog(); |
607 | m_editor->setFocus(); |
608 | } |
609 | |
610 | void RichTextEditorToolBar::insertImage() |
611 | { |
612 | const QString path = IconSelector::choosePixmapResource(core: m_core, resourceModel: m_core->resourceModel(), oldPath: QString(), parent: this); |
613 | if (!path.isEmpty()) |
614 | m_editor->insertHtml(QStringLiteral("<img src=\"" ) + path + QStringLiteral("\"/>" )); |
615 | } |
616 | |
617 | void RichTextEditorToolBar::layoutDirectionChanged() |
618 | { |
619 | QTextCursor cursor = m_editor->textCursor(); |
620 | QTextBlock block = cursor.block(); |
621 | if (block.isValid()) { |
622 | QTextBlockFormat format = block.blockFormat(); |
623 | const Qt::LayoutDirection newDirection = m_layoutDirectionAction->isChecked() ? Qt::RightToLeft : Qt::LeftToRight; |
624 | if (format.layoutDirection() != newDirection) { |
625 | format.setLayoutDirection(newDirection); |
626 | cursor.setBlockFormat(format); |
627 | } |
628 | } |
629 | } |
630 | |
631 | void RichTextEditorToolBar::updateActions() |
632 | { |
633 | if (m_editor == nullptr) { |
634 | setEnabled(false); |
635 | return; |
636 | } |
637 | |
638 | const Qt::Alignment alignment = m_editor->alignment(); |
639 | const QTextCursor cursor = m_editor->textCursor(); |
640 | const QTextCharFormat charFormat = cursor.charFormat(); |
641 | const QFont font = charFormat.font(); |
642 | const QTextCharFormat::VerticalAlignment valign = |
643 | charFormat.verticalAlignment(); |
644 | const bool superScript = valign == QTextCharFormat::AlignSuperScript; |
645 | const bool subScript = valign == QTextCharFormat::AlignSubScript; |
646 | |
647 | if (alignment & Qt::AlignLeft) { |
648 | m_align_left_action->setChecked(true); |
649 | } else if (alignment & Qt::AlignRight) { |
650 | m_align_right_action->setChecked(true); |
651 | } else if (alignment & Qt::AlignHCenter) { |
652 | m_align_center_action->setChecked(true); |
653 | } else { |
654 | m_align_justify_action->setChecked(true); |
655 | } |
656 | m_layoutDirectionAction->setChecked(cursor.blockFormat().layoutDirection() == Qt::RightToLeft); |
657 | |
658 | m_bold_action->setChecked(font.bold()); |
659 | m_italic_action->setChecked(font.italic()); |
660 | m_underline_action->setChecked(font.underline()); |
661 | m_valign_sup_action->setChecked(superScript); |
662 | m_valign_sub_action->setChecked(subScript); |
663 | |
664 | const int size = font.pointSize(); |
665 | const int idx = m_font_size_input->findText(text: QString::number(size)); |
666 | if (idx != -1) |
667 | m_font_size_input->setCurrentIndex(idx); |
668 | |
669 | m_color_action->setColor(m_editor->textColor()); |
670 | } |
671 | |
672 | RichTextEditor::RichTextEditor(QWidget *parent) |
673 | : QTextEdit(parent), m_simplifyRichText(simplifyRichTextDefault) |
674 | { |
675 | connect(sender: this, signal: &RichTextEditor::currentCharFormatChanged, |
676 | receiver: this, slot: &RichTextEditor::stateChanged); |
677 | connect(sender: this, signal: &RichTextEditor::cursorPositionChanged, |
678 | receiver: this, slot: &RichTextEditor::stateChanged); |
679 | } |
680 | |
681 | QToolBar *RichTextEditor::createToolBar(QDesignerFormEditorInterface *core, QWidget *parent) |
682 | { |
683 | return new RichTextEditorToolBar(core, this, parent); |
684 | } |
685 | |
686 | void RichTextEditor::setFontBold(bool b) |
687 | { |
688 | if (b) |
689 | setFontWeight(QFont::Bold); |
690 | else |
691 | setFontWeight(QFont::Normal); |
692 | } |
693 | |
694 | void RichTextEditor::setFontPointSize(double d) |
695 | { |
696 | QTextEdit::setFontPointSize(qreal(d)); |
697 | } |
698 | |
699 | void RichTextEditor::setText(const QString &text) |
700 | { |
701 | |
702 | if (Qt::mightBeRichText(text)) |
703 | setHtml(text); |
704 | else |
705 | setPlainText(text); |
706 | } |
707 | |
708 | void RichTextEditor::setSimplifyRichText(bool v) |
709 | { |
710 | if (v != m_simplifyRichText) { |
711 | m_simplifyRichText = v; |
712 | emit simplifyRichTextChanged(v); |
713 | } |
714 | } |
715 | |
716 | void RichTextEditor::setDefaultFont(QFont font) |
717 | { |
718 | // Some default fonts on Windows have a default size of 7.8, |
719 | // which results in complicated rich text generated by toHtml(). |
720 | // Use an integer value. |
721 | const int pointSize = qRound(d: font.pointSizeF()); |
722 | if (pointSize > 0 && !qFuzzyCompare(p1: qreal(pointSize), p2: font.pointSizeF())) { |
723 | font.setPointSize(pointSize); |
724 | } |
725 | |
726 | document()->setDefaultFont(font); |
727 | if (font.pointSize() > 0) |
728 | setFontPointSize(font.pointSize()); |
729 | else |
730 | setFontPointSize(QFontInfo(font).pointSize()); |
731 | emit textChanged(); |
732 | } |
733 | |
734 | QString RichTextEditor::text(Qt::TextFormat format) const |
735 | { |
736 | switch (format) { |
737 | case Qt::PlainText: |
738 | return toPlainText(); |
739 | case Qt::RichText: |
740 | return m_simplifyRichText ? simplifyRichTextFilter(in: toHtml()) : toHtml(); |
741 | default: |
742 | break; |
743 | } |
744 | const QString html = toHtml(); |
745 | bool isPlainText; |
746 | const QString simplifiedHtml = simplifyRichTextFilter(in: html, isPlainTextPtr: &isPlainText); |
747 | if (isPlainText) |
748 | return toPlainText(); |
749 | return m_simplifyRichText ? simplifiedHtml : html; |
750 | } |
751 | |
752 | RichTextEditorDialog::RichTextEditorDialog(QDesignerFormEditorInterface *core, QWidget *parent) : |
753 | QDialog(parent), |
754 | m_editor(new RichTextEditor()), |
755 | m_text_edit(new HtmlTextEdit), |
756 | m_tab_widget(new QTabWidget), |
757 | m_state(Clean), |
758 | m_core(core), |
759 | m_initialTab(RichTextIndex) |
760 | { |
761 | setWindowTitle(tr(s: "Edit text" )); |
762 | setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); |
763 | |
764 | // Read settings |
765 | const QDesignerSettingsInterface *settings = core->settingsManager(); |
766 | const QString rootKey = QLatin1String(RichTextDialogGroupC) + QLatin1Char('/'); |
767 | const QByteArray lastGeometry = settings->value(key: rootKey + QLatin1String(GeometryKeyC)).toByteArray(); |
768 | const int initialTab = settings->value(key: rootKey + QLatin1String(TabKeyC), defaultValue: QVariant(m_initialTab)).toInt(); |
769 | if (initialTab == RichTextIndex || initialTab == SourceIndex) |
770 | m_initialTab = initialTab; |
771 | |
772 | m_text_edit->setAcceptRichText(false); |
773 | new HtmlHighlighter(m_text_edit); |
774 | |
775 | connect(sender: m_editor, signal: &QTextEdit::textChanged, receiver: this, slot: &RichTextEditorDialog::richTextChanged); |
776 | connect(sender: m_editor, signal: &RichTextEditor::simplifyRichTextChanged, |
777 | receiver: this, slot: &RichTextEditorDialog::richTextChanged); |
778 | connect(sender: m_text_edit, signal: &QTextEdit::textChanged, receiver: this, slot: &RichTextEditorDialog::sourceChanged); |
779 | |
780 | // The toolbar needs to be created after the RichTextEditor |
781 | QToolBar *tool_bar = m_editor->createToolBar(core); |
782 | tool_bar->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Minimum); |
783 | |
784 | QWidget *rich_edit = new QWidget; |
785 | QVBoxLayout *rich_edit_layout = new QVBoxLayout(rich_edit); |
786 | rich_edit_layout->addWidget(tool_bar); |
787 | rich_edit_layout->addWidget(m_editor); |
788 | |
789 | QWidget *plain_edit = new QWidget; |
790 | QVBoxLayout *plain_edit_layout = new QVBoxLayout(plain_edit); |
791 | plain_edit_layout->addWidget(m_text_edit); |
792 | |
793 | m_tab_widget->setTabPosition(QTabWidget::South); |
794 | m_tab_widget->addTab(widget: rich_edit, tr(s: "Rich Text" )); |
795 | m_tab_widget->addTab(widget: plain_edit, tr(s: "Source" )); |
796 | connect(sender: m_tab_widget, signal: &QTabWidget::currentChanged, |
797 | receiver: this, slot: &RichTextEditorDialog::tabIndexChanged); |
798 | |
799 | QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal); |
800 | QPushButton *ok_button = buttonBox->button(which: QDialogButtonBox::Ok); |
801 | ok_button->setText(tr(s: "&OK" )); |
802 | ok_button->setDefault(true); |
803 | buttonBox->button(which: QDialogButtonBox::Cancel)->setText(tr(s: "&Cancel" )); |
804 | connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, receiver: this, slot: &QDialog::accept); |
805 | connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, receiver: this, slot: &QDialog::reject); |
806 | |
807 | QVBoxLayout *layout = new QVBoxLayout(this); |
808 | layout->addWidget(m_tab_widget); |
809 | layout->addWidget(buttonBox); |
810 | |
811 | if (!lastGeometry.isEmpty()) |
812 | restoreGeometry(geometry: lastGeometry); |
813 | } |
814 | |
815 | RichTextEditorDialog::~RichTextEditorDialog() |
816 | { |
817 | QDesignerSettingsInterface *settings = m_core->settingsManager(); |
818 | settings->beginGroup(prefix: QLatin1String(RichTextDialogGroupC)); |
819 | |
820 | settings->setValue(key: QLatin1String(GeometryKeyC), value: saveGeometry()); |
821 | settings->setValue(key: QLatin1String(TabKeyC), value: m_tab_widget->currentIndex()); |
822 | settings->endGroup(); |
823 | } |
824 | |
825 | int RichTextEditorDialog::showDialog() |
826 | { |
827 | m_tab_widget->setCurrentIndex(m_initialTab); |
828 | switch (m_initialTab) { |
829 | case RichTextIndex: |
830 | m_editor->selectAll(); |
831 | m_editor->setFocus(); |
832 | break; |
833 | case SourceIndex: |
834 | m_text_edit->selectAll(); |
835 | m_text_edit->setFocus(); |
836 | break; |
837 | } |
838 | return exec(); |
839 | } |
840 | |
841 | void RichTextEditorDialog::setDefaultFont(const QFont &font) |
842 | { |
843 | m_editor->setDefaultFont(font); |
844 | } |
845 | |
846 | void RichTextEditorDialog::setText(const QString &text) |
847 | { |
848 | // Generally simplify rich text unless verbose text is found. |
849 | const bool isSimplifiedRichText = !text.startsWith(QStringLiteral("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">" )); |
850 | m_editor->setSimplifyRichText(isSimplifiedRichText); |
851 | m_editor->setText(text); |
852 | m_text_edit->setPlainText(text); |
853 | m_state = Clean; |
854 | } |
855 | |
856 | QString RichTextEditorDialog::text(Qt::TextFormat format) const |
857 | { |
858 | // In autotext mode, if the user has changed the source, use that |
859 | if (format == Qt::AutoText && (m_state == Clean || m_state == SourceChanged)) |
860 | return m_text_edit->toPlainText(); |
861 | // If the plain text HTML editor is selected, first copy its contents over |
862 | // to the rich text editor so that it is converted to Qt-HTML or actual |
863 | // plain text. |
864 | if (m_tab_widget->currentIndex() == SourceIndex && m_state == SourceChanged) |
865 | m_editor->setHtml(m_text_edit->toPlainText()); |
866 | return m_editor->text(format); |
867 | } |
868 | |
869 | void RichTextEditorDialog::tabIndexChanged(int newIndex) |
870 | { |
871 | // Anything changed, is there a need for a conversion? |
872 | if (newIndex == SourceIndex && m_state != RichTextChanged) |
873 | return; |
874 | if (newIndex == RichTextIndex && m_state != SourceChanged) |
875 | return; |
876 | const State oldState = m_state; |
877 | // Remember the cursor position, since it is invalidated by setPlainText |
878 | QTextEdit *new_edit = (newIndex == SourceIndex) ? m_text_edit : m_editor; |
879 | const int position = new_edit->textCursor().position(); |
880 | |
881 | if (newIndex == SourceIndex) |
882 | m_text_edit->setPlainText(m_editor->text(format: Qt::RichText)); |
883 | else |
884 | m_editor->setHtml(m_text_edit->toPlainText()); |
885 | |
886 | QTextCursor cursor = new_edit->textCursor(); |
887 | cursor.movePosition(op: QTextCursor::End); |
888 | if (cursor.position() > position) { |
889 | cursor.setPosition(pos: position); |
890 | } |
891 | new_edit->setTextCursor(cursor); |
892 | m_state = oldState; // Changed is triggered by setting the text |
893 | } |
894 | |
895 | void RichTextEditorDialog::richTextChanged() |
896 | { |
897 | m_state = RichTextChanged; |
898 | } |
899 | |
900 | void RichTextEditorDialog::sourceChanged() |
901 | { |
902 | m_state = SourceChanged; |
903 | } |
904 | |
905 | } // namespace qdesigner_internal |
906 | |
907 | QT_END_NAMESPACE |
908 | |
909 | #include "richtexteditor.moc" |
910 | |