1 /**********************************************************************************************
2     Copyright (C) 2016 Christian Eichler <code@christian-eichler.de>
3 
4     Copyright (C) 2012 Digia Plc and/or its subsidiaries <>
5     Contact: http://www.qt-project.org/legal
6 
7     This program is free software: you can redistribute it and/or modify
8     it under the terms of the GNU General Public License as published by
9     the Free Software Foundation, version 3 of the License.
10 
11     This program is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 
19 **********************************************************************************************/
20 
21 
22 #include "CTextEditWidget.h"
23 #include "helpers/CSettings.h"
24 #include "helpers/Signals.h"
25 #include "widgets/CTemplateWidget.h"
26 #include "widgets/CTextEditWidgetSelMenu.h"
27 
28 #include <QtWidgets>
29 
CTextEditWidget(const QString & html,QWidget * parent)30 CTextEditWidget::CTextEditWidget(const QString& html, QWidget* parent)
31     : QDialog(parent)
32 {
33     SETTINGS;
34 
35     bool pastePlain = cfg.value("TextEditWidget/pastePlain", false).toBool();
36 
37     setupUi(this);
38 
39     selectionWindow = new CTextEditWidgetSelMenu(this,
40                                                  /* font style actions */ actionTextBold, actionTextItalic, actionTextUnderline,
41                                                  /* copy/paste actions */ actionCut, actionCopy, actionPaste
42                                                  );
43 
44     QScrollBar* vbar = textEdit->verticalScrollBar();
45     connect(vbar, &QAbstractSlider::valueChanged, this, &CTextEditWidget::textEditScrolled);
46 
47     toolBold->setDefaultAction  (actionTextBold);
48     toolItalic->setDefaultAction(actionTextItalic);
49     toolUnder->setDefaultAction (actionTextUnderline);
50 
51     connect(actionTextBold, &QAction::triggered, this, &CTextEditWidget::textBold);
52     connect(actionTextItalic, &QAction::triggered, this, &CTextEditWidget::textItalic);
53     connect(actionTextUnderline, &QAction::triggered, this, &CTextEditWidget::textUnderline);
54 
55     QActionGroup* grp = new QActionGroup(this);
56     grp->addAction(actionAlignLeft);
57     grp->addAction(actionAlignRight);
58     grp->addAction(actionAlignCenter);
59     grp->addAction(actionAlignJustify);
60     connect(grp, &QActionGroup::triggered, this, &CTextEditWidget::textAlign);
61 
62     toolLeft->setDefaultAction(actionAlignLeft);
63     toolCenter->setDefaultAction(actionAlignCenter);
64     toolRight->setDefaultAction(actionAlignRight);
65     toolBlock->setDefaultAction(actionAlignJustify);
66 
67     defaultFont = textEdit->font();
68 
69     QPixmap pix(24, 24);
70     pix.fill(Qt::black);
71     actionTextColor = new QAction(pix, tr("&Color..."), this);
72     connect(actionTextColor, &QAction::triggered, this, &CTextEditWidget::textColor);
73     toolColor->setDefaultAction(actionTextColor);
74 
75     connect(comboStyle, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &CTextEditWidget::textStyle);
76 
77     connect(comboFont, &QFontComboBox::currentFontChanged, textEdit, &QTextEdit::setCurrentFont);
78     connect(spinFontSize, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), textEdit, &QTextEdit::setFontPointSize);
79 
80     connect(textEdit, &QTextEdit::currentCharFormatChanged, this, &CTextEditWidget::currentCharFormatChanged);
81     connect(textEdit, &QTextEdit::cursorPositionChanged, this, &CTextEditWidget::cursorPositionChanged);
82     connect(textEdit, &QTextEdit::textChanged, this, &CTextEditWidget::cursorPositionChanged);
83     connect(textEdit, &QTextEdit::selectionChanged, this, &CTextEditWidget::selectionChanged);
84 
85     textEdit->setHtml(html);
86     textEdit->setFocus();
87 
88     fontChanged(textEdit->font());
89     colorChanged(textEdit->textColor());
90     alignmentChanged(textEdit->alignment());
91 
92     toolInsertFromTemplate->setDefaultAction(actionInsertFromTemplate);
93     toolUndo->setDefaultAction(actionUndo);
94     toolRedo->setDefaultAction(actionRedo);
95     toolCut->setDefaultAction(actionCut);
96     toolCopy->setDefaultAction(actionCopy);
97     toolPaste->setDefaultAction(actionPaste);
98 
99     QMenu* menu = new QMenu(this);
100     menu->addAction(actionPastePlain);
101     menu->addAction(actionPasteNormal);
102     toolPaste->setMenu(menu);
103 
104     /* Setup contextmenu for textEdit */
105     menuTextEdit = new QMenu(this);
106     menuTextEdit->addAction(actionInsertFromTemplate);
107     menuTextEdit->addSeparator();
108     menuTextEdit->addAction(actionUndo);
109     menuTextEdit->addAction(actionRedo);
110     menuTextEdit->addSeparator();
111     menuTextEdit->addAction(actionCut);
112     menuTextEdit->addAction(actionCopy);
113     menuTextEdit->addAction(actionPaste);
114     menuTextEdit->addAction(actionDelete);
115     menuTextEdit->addSeparator();
116 
117     removeFormat = new QMenu(tr("Reset format"), this);
118     {
119         menuTextEdit->addMenu(removeFormat);
120         removeFormat->addAction(actionResetFont);
121         removeFormat->addAction(actionResetLayout);
122     }
123 
124     connect(actionResetFont, &QAction::triggered, this, &CTextEditWidget::resetFont);
125     connect(actionResetLayout, &QAction::triggered, this, &CTextEditWidget::resetLayout);
126 
127     menuTextEdit->addAction(actionSelectAll);
128 
129     actionPaste->setEnabled(!QApplication::clipboard()->text().isEmpty());
130     actionPastePlain->setEnabled(!QApplication::clipboard()->text().isEmpty());
131     actionUndo->setEnabled(textEdit->document()->isUndoAvailable());
132     actionRedo->setEnabled(textEdit->document()->isRedoAvailable());
133 
134     connect(textEdit->document(), &QTextDocument::undoAvailable, actionUndo, &QAction::setEnabled);
135     connect(textEdit->document(), &QTextDocument::redoAvailable, actionRedo, &QAction::setEnabled);
136 
137     connect(actionInsertFromTemplate, &QAction::triggered, this, &CTextEditWidget::insertFromTemplate);
138     connect(actionUndo, &QAction::triggered, textEdit, &QTextEdit::undo);
139     connect(actionRedo, &QAction::triggered, textEdit, &QTextEdit::redo);
140 
141     actionCut->setEnabled(false);
142     actionCopy->setEnabled(false);
143 
144     QActionGroup* pasteGroup = new QActionGroup(this);
145     actionPastePlain->setChecked(pastePlain);
146     actionPasteNormal->setChecked(!pastePlain);
147     pasteGroup->addAction(actionPastePlain);
148     pasteGroup->addAction(actionPasteNormal);
149     connect(pasteGroup, &QActionGroup::triggered, this, &CTextEditWidget::pasteMode);
150 
151     pasteMode(pastePlain ? actionPastePlain : actionPasteNormal);
152 
153     connect(actionCut, &QAction::triggered, textEdit, &QTextEdit::cut);
154     connect(actionCopy, &QAction::triggered, textEdit, &QTextEdit::copy);
155     connect(actionSelectAll, &QAction::triggered, textEdit, &QTextEdit::selectAll);
156     connect(actionPaste, &QAction::triggered, textEdit, &CTextEdit::paste);
157     connect(actionDelete, &QAction::triggered, this, &CTextEditWidget::deleteSelected);
158     connect(textEdit, &QTextEdit::customContextMenuRequested, this, &CTextEditWidget::customContextMenuRequested);
159     connect(textEdit, &QTextEdit::copyAvailable, actionCut, &QAction::setEnabled);
160     connect(textEdit, &QTextEdit::copyAvailable, actionCopy, &QAction::setEnabled);
161 
162     connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &CTextEditWidget::clipboardDataChanged);
163 }
164 
~CTextEditWidget()165 CTextEditWidget::~CTextEditWidget()
166 {
167     SETTINGS;
168     cfg.setValue("TextEditWidget/pastePlain", actionPastePlain->isChecked());
169 }
170 
getHtml()171 QString CTextEditWidget::getHtml()
172 {
173     QString str = textEdit->toHtml();
174     QRegExp re(".*(\\<body.*body\\>).*");
175     if(re.exactMatch(str))
176     {
177         str = re.cap(1);
178 
179         QRegExp re1("<body.*>");
180         re1.setMinimal(true);
181         str = str.replace("body>", "div>").replace(re1, "<div>");
182     }
183 
184     return str;
185 }
186 
187 
textBold()188 void CTextEditWidget::textBold()
189 {
190     QTextCharFormat fmt;
191     fmt.setFontWeight(actionTextBold->isChecked() ? QFont::Bold : QFont::Normal);
192     mergeFormatOnWordOrSelection(fmt);
193 }
194 
195 
textUnderline()196 void CTextEditWidget::textUnderline()
197 {
198     QTextCharFormat fmt;
199     fmt.setFontUnderline(actionTextUnderline->isChecked());
200     mergeFormatOnWordOrSelection(fmt);
201 }
202 
203 
textItalic()204 void CTextEditWidget::textItalic()
205 {
206     QTextCharFormat fmt;
207     fmt.setFontItalic(actionTextItalic->isChecked());
208     mergeFormatOnWordOrSelection(fmt);
209 }
210 
211 
textAlign(QAction * a)212 void CTextEditWidget::textAlign(QAction* a)
213 {
214     if (a == actionAlignLeft)
215     {
216         textEdit->setAlignment(Qt::AlignLeft);
217     }
218     else if (a == actionAlignCenter)
219     {
220         textEdit->setAlignment(Qt::AlignHCenter);
221     }
222     else if (a == actionAlignRight)
223     {
224         textEdit->setAlignment(Qt::AlignRight);
225     }
226     else if (a == actionAlignJustify)
227     {
228         textEdit->setAlignment(Qt::AlignJustify);
229     }
230 }
231 
232 
233 
textStyle(int styleIndex)234 void CTextEditWidget::textStyle(int styleIndex)
235 {
236     if (styleIndex > 0)
237     {
238         QTextCursor cursor = textEdit->textCursor();
239         QTextListFormat::Style style = QTextListFormat::ListDisc;
240 
241         static QTextListFormat::Style indexToFormat[] =
242         {
243             QTextListFormat::ListDisc,
244             QTextListFormat::ListCircle,
245             QTextListFormat::ListSquare,
246             QTextListFormat::ListDecimal,
247             QTextListFormat::ListLowerAlpha,
248             QTextListFormat::ListUpperAlpha,
249             QTextListFormat::ListLowerRoman,
250             QTextListFormat::ListUpperRoman
251         };
252 
253         if( (unsigned) styleIndex <= sizeof(indexToFormat) / sizeof(QTextListFormat::Style))
254         {
255             style = indexToFormat[styleIndex - 1];
256         }
257 
258         cursor.beginEditBlock();
259 
260         QTextBlockFormat blockFmt = cursor.blockFormat();
261 
262         QTextListFormat listFmt;
263 
264         if (cursor.currentList())
265         {
266             listFmt = cursor.currentList()->format();
267         }
268         else
269         {
270             listFmt.setIndent(blockFmt.indent() + 1);
271             blockFmt.setIndent(0);
272             cursor.setBlockFormat(blockFmt);
273         }
274 
275         listFmt.setStyle(style);
276 
277         cursor.createList(listFmt);
278 
279         cursor.endEditBlock();
280     }
281     else
282     {
283         resetLayout();
284     }
285 }
286 
resetLayout()287 void CTextEditWidget::resetLayout()
288 {
289     textEdit->textCursor().setBlockFormat(QTextBlockFormat());
290 }
291 
resetFont()292 void CTextEditWidget::resetFont()
293 {
294     QTextCharFormat fmt;
295     fmt.setFontUnderline(false);
296     fmt.setFontWeight(QFont::Normal);
297     fmt.setFontItalic(false);
298     fmt.setForeground(QColor());
299 
300     fmt.setFont(defaultFont);
301     fmt.setFontPointSize(defaultFont.pointSizeF());
302 
303     QTextCursor cursor = textEdit->textCursor();
304     if (!cursor.hasSelection())
305     {
306         cursor.select(QTextCursor::WordUnderCursor);
307     }
308     cursor.setCharFormat(fmt);
309 
310     fontChanged(defaultFont);
311     colorChanged(QColor());
312 }
313 
textColor()314 void CTextEditWidget::textColor()
315 {
316     QColor col = QColorDialog::getColor(textEdit->textColor(), this);
317     if (!col.isValid())
318     {
319         return;
320     }
321     QTextCharFormat fmt;
322     fmt.setForeground(col);
323     mergeFormatOnWordOrSelection(fmt);
324     colorChanged(col);
325 }
326 
327 
mergeFormatOnWordOrSelection(const QTextCharFormat & format)328 void CTextEditWidget::mergeFormatOnWordOrSelection(const QTextCharFormat& format)
329 {
330     QTextCursor cursor = textEdit->textCursor();
331     if (!cursor.hasSelection())
332     {
333         cursor.select(QTextCursor::WordUnderCursor);
334     }
335     cursor.mergeCharFormat(format);
336     textEdit->mergeCurrentCharFormat(format);
337 }
338 
339 
fontChanged(const QFont & f)340 void CTextEditWidget::fontChanged(const QFont& f)
341 {
342     actionTextBold->setChecked(f.bold());
343     actionTextItalic->setChecked(f.italic());
344     actionTextUnderline->setChecked(f.underline());
345 }
346 
347 
colorChanged(const QColor & c)348 void CTextEditWidget::colorChanged(const QColor& c)
349 {
350     QPixmap pix(16, 16);
351     pix.fill(c);
352     actionTextColor->setIcon(pix);
353 }
354 
355 
alignmentChanged(Qt::Alignment a)356 void CTextEditWidget::alignmentChanged(Qt::Alignment a)
357 {
358     if (a & Qt::AlignLeft)
359     {
360         actionAlignLeft->setChecked(true);
361     }
362     else if (a & Qt::AlignHCenter)
363     {
364         actionAlignCenter->setChecked(true);
365     }
366     else if (a & Qt::AlignRight)
367     {
368         actionAlignRight->setChecked(true);
369     }
370     else if (a & Qt::AlignJustify)
371     {
372         actionAlignJustify->setChecked(true);
373     }
374 }
375 
376 
currentCharFormatChanged(const QTextCharFormat & format)377 void CTextEditWidget::currentCharFormatChanged(const QTextCharFormat& format)
378 {
379     fontChanged(format.font());
380     colorChanged(format.foreground().color());
381 }
382 
383 
cursorPositionChanged()384 void CTextEditWidget::cursorPositionChanged()
385 {
386     static QHash<QTextListFormat::Style, int> styleToIndex({
387         std::make_pair(QTextListFormat::ListDisc, 1),
388         std::make_pair(QTextListFormat::ListCircle, 2),
389         std::make_pair(QTextListFormat::ListSquare, 3),
390         std::make_pair(QTextListFormat::ListDecimal, 4),
391         std::make_pair(QTextListFormat::ListLowerAlpha, 5),
392         std::make_pair(QTextListFormat::ListUpperAlpha, 6),
393         std::make_pair(QTextListFormat::ListLowerRoman, 7),
394         std::make_pair(QTextListFormat::ListUpperRoman, 8)
395     });
396 
397     alignmentChanged(textEdit->alignment());
398 
399     int listStyleIndex = 0;
400 
401     QTextCursor cursor = textEdit->textCursor();
402     if(cursor.currentList())
403     {
404         QTextListFormat::Style style = cursor.currentList()->format().style();
405 
406         if(styleToIndex.contains(style))
407         {
408             listStyleIndex = styleToIndex[ cursor.currentList()->format().style() ];
409         }
410     }
411 
412     X______________BlockAllSignals______________X(this);
413     comboStyle->setCurrentIndex(listStyleIndex);
414 
415     const QFont& font = cursor.charFormat().font();
416     comboFont->setCurrentFont(font);
417 
418     int pointSize = font.pointSize();
419 
420     if(-1 == pointSize)
421     {
422         // some texts (if pasted from px. a browser) have their font size
423         // specified in pixels instead of points, so we need to convert that
424         QFontInfo info(font);
425         pointSize = info.pointSize();
426     }
427     spinFontSize->setValue(pointSize);
428 
429     X_____________UnBlockAllSignals_____________X(this);
430 }
431 
432 
clipboardDataChanged()433 void CTextEditWidget::clipboardDataChanged()
434 {
435     actionPaste->setEnabled(!QApplication::clipboard()->text().isEmpty());
436     actionPastePlain->setEnabled(!QApplication::clipboard()->text().isEmpty());
437 }
438 
439 
selectionChanged()440 void CTextEditWidget::selectionChanged()
441 {
442     bool hasSel = textEdit->textCursor().hasSelection();
443 
444     actionDelete->setEnabled     (hasSel);
445     removeFormat->setEnabled     (hasSel);
446     actionResetFont->setEnabled  (hasSel);
447     actionResetLayout->setEnabled(hasSel);
448 
449     updateSelectionWindow();
450 }
451 
customContextMenuRequested()452 void CTextEditWidget::customContextMenuRequested()
453 {
454     menuTextEdit->exec(QCursor::pos());
455 }
456 
deleteSelected()457 void CTextEditWidget::deleteSelected()
458 {
459     textEdit->insertPlainText(QString());
460 }
461 
textEditScrolled()462 void CTextEditWidget::textEditScrolled()
463 {
464     updateSelectionWindow();
465 }
466 
moveEvent(QMoveEvent * event)467 void CTextEditWidget::moveEvent(QMoveEvent* event)
468 {
469     updateSelectionWindow();
470 }
471 
pasteMode(QAction * action)472 void CTextEditWidget::pasteMode(QAction* action)
473 {
474     textEdit->setPastePlain( action == actionPastePlain );
475 
476     actionPaste->setIcon(action->icon());
477 }
478 
event(QEvent * event)479 bool CTextEditWidget::event(QEvent* event)
480 {
481     if(event->type() == QEvent::WindowActivate)
482     {
483         updateSelectionWindow();
484     }
485     else if(event->type() == QEvent::WindowDeactivate)
486     {
487         selectionWindow->hide();
488     }
489     return QDialog::event(event);
490 }
491 
updateSelectionWindow()492 void CTextEditWidget::updateSelectionWindow()
493 {
494     const QTextCursor& cursor = textEdit->textCursor();
495     const QRect& rect = textEdit->cursorRect();
496 
497     // don't show the selctionWindow, if there is no selection or
498     // the cursor is not visible
499     if(cursor.hasSelection() && rect.y() >= 0 && rect.y() <= textEdit->height())
500     {
501         int dy = cursor.anchor() < cursor.position()
502                  ? (  6 + rect.height() )
503                  : ( -6 - selectionWindow->height() );
504 
505         int dx = -selectionWindow->width() / 2;
506 
507         selectionWindow->move(textEdit->mapToGlobal(QPoint(rect.x(), rect.y())) + QPoint(dx, dy));
508         selectionWindow->show();
509     }
510     else
511     {
512         selectionWindow->hide();
513     }
514 }
515 
516 
insertFromTemplate()517 void CTextEditWidget::insertFromTemplate()
518 {
519     CTemplateWidget dlg(this);
520     if(dlg.exec() == QDialog::Accepted)
521     {
522         textEdit->insertHtml(dlg.text());
523     }
524 }
525