Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
messageeditorwidgets.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
6#include "globals.h"
7
8#include <translator.h>
9
10#include <QAbstractTextDocumentLayout>
11#include <QAction>
12#include <QApplication>
13#include <QClipboard>
14#include <QDebug>
15#include <QLayout>
16#include <QMenu>
17#include <QMessageBox>
18#include <QPainter>
19#include <QScrollArea>
20#include <QTextBlock>
21#include <QTextDocumentFragment>
22#include <QToolButton>
23#include <QVBoxLayout>
24#include <QtGui/private/qtextdocument_p.h>
25
27
28using namespace Qt::Literals::StringLiterals;
29
30ExpandingTextEdit::ExpandingTextEdit(QWidget *parent)
31 : QTextEdit(parent)
32{
33 setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding));
34
35 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
36 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
37
38 QAbstractTextDocumentLayout *docLayout = document()->documentLayout();
39 connect(docLayout, &QAbstractTextDocumentLayout::documentSizeChanged,
40 this, &ExpandingTextEdit::updateHeight);
41 connect(this, &QTextEdit::cursorPositionChanged,
42 this, &ExpandingTextEdit::reallyEnsureCursorVisible);
43
44 m_minimumHeight = qRound(docLayout->documentSize().height()) + frameWidth() * 2;
45}
46
47void ExpandingTextEdit::updateHeight(const QSizeF &documentSize)
48{
49 m_minimumHeight = qRound(documentSize.height()) + frameWidth() * 2;
50 updateGeometry();
51}
52
54{
55 return QSize(100, m_minimumHeight);
56}
57
59{
60 return QSize(100, m_minimumHeight);
61}
62
63void ExpandingTextEdit::reallyEnsureCursorVisible()
64{
65 QObject *ancestor = parent();
66 while (ancestor) {
67 QScrollArea *scrollArea = qobject_cast<QScrollArea*>(ancestor);
68 if (scrollArea &&
69 (scrollArea->verticalScrollBarPolicy() != Qt::ScrollBarAlwaysOff &&
70 scrollArea->horizontalScrollBarPolicy() != Qt::ScrollBarAlwaysOff)) {
71 const QRect &r = cursorRect();
72 const QPoint &c = mapTo(scrollArea->widget(), r.center());
73 scrollArea->ensureVisible(c.x(), c.y());
74 break;
75 }
76 ancestor = ancestor->parent();
77 }
78}
79
80FormatTextEdit::FormatTextEdit(QWidget *parent)
81 : ExpandingTextEdit(parent)
82{
83 setLineWrapMode(QTextEdit::WidgetWidth);
84 setAcceptRichText(false);
85 m_highlighter = new MessageHighlighter(this);
86
87 // Do not set different background if disabled
88 QPalette p = palette();
89 p.setColor(QPalette::Disabled, QPalette::Base, p.color(QPalette::Active, QPalette::Base));
90 setPalette(p);
91
92 m_defaultPalette = p;
93
94 setEditable(true);
95}
96
98{
99 emit editorDestroyed();
100}
101
102void FormatTextEdit::setEditable(bool editable)
103{
104 // save default frame style
105 static int framed = frameStyle();
106 static Qt::FocusPolicy defaultFocus = focusPolicy();
107
108 if (editable) {
109 setFrameStyle(framed);
110 setFocusPolicy(defaultFocus);
111 } else {
112 setFrameStyle(QFrame::NoFrame | QFrame::Plain);
113 setFocusPolicy(Qt::NoFocus);
114 }
115
116 setReadOnly(!editable);
117 applyReadOnlySelectionPalette();
118}
119
120void FormatTextEdit::setPlainText(const QString &text, bool userAction)
121{
122 if (!userAction) {
123 // Prevent contentsChanged signal
124 bool oldBlockState = blockSignals(true);
125 document()->setUndoRedoEnabled(false);
126 ExpandingTextEdit::setPlainText(text);
127 // highlighter is out of sync because of blocked signals
128 m_highlighter->rehighlight();
129 document()->setUndoRedoEnabled(true);
130 blockSignals(oldBlockState);
131 } else {
132 ExpandingTextEdit::setPlainText(text);
133 }
134}
135
137{
138 QTextOption option = document()->defaultTextOption();
139 if (value) {
140 option.setFlags(option.flags()
141 | QTextOption::ShowLineAndParagraphSeparators
142 | QTextOption::ShowTabsAndSpaces);
143 } else {
144 option.setFlags(option.flags()
145 & ~QTextOption::ShowLineAndParagraphSeparators
146 & ~QTextOption::ShowTabsAndSpaces);
147 }
148 document()->setDefaultTextOption(option);
149}
150
151bool FormatTextEdit::event(QEvent *event)
152{
153 if ((event->type() == QEvent::ApplicationPaletteChange
154 || event->type() == QEvent::PaletteChange)) {
155 m_highlighter->adjustColors();
156 m_defaultPalette = palette();
157 applyReadOnlySelectionPalette();
158 }
159 return ExpandingTextEdit::event(event);
160}
161
162void FormatTextEdit::applyReadOnlySelectionPalette()
163{
164 // Apply a gray selection background for read-only text in dark mode.
165 // Keep editable widgets using the default/current palette behavior.
166 if (!isReadOnly()) {
167 setPalette(m_defaultPalette);
168 return;
169 }
170
171 QPalette pal = m_defaultPalette;
172 if (isDarkMode()) {
173 const QColor gray(100, 100, 100);
174 const QColor darkText = pal.color(QPalette::Text);
175 pal.setColor(QPalette::Inactive, QPalette::Highlight, gray);
176 pal.setColor(QPalette::Inactive, QPalette::HighlightedText, darkText);
177 pal.setColor(QPalette::Disabled, QPalette::Highlight, gray);
178 pal.setColor(QPalette::Disabled, QPalette::HighlightedText, darkText);
179 }
180 setPalette(pal);
181}
182
183FormWidget::FormWidget(const QString &label, bool isEditable, QWidget *parent)
184 : QWidget(parent),
185 m_hideWhenEmpty(false)
186{
187 QVBoxLayout *layout = new QVBoxLayout;
188 layout->setContentsMargins(QMargins());
189
190 m_label = new QLabel(this);
191 QFont fnt;
192 fnt.setBold(true);
193 m_label->setFont(fnt);
194 m_label->setText(label);
195 layout->addWidget(m_label);
196
197 m_editor = new FormatTextEdit(this);
198 m_editor->setEditable(isEditable);
199 //m_textEdit->setWhatsThis(tr("This area shows text from an auxillary translation."));
200 layout->addWidget(m_editor);
201
202 setLayout(layout);
203
204 connect(m_editor, &QTextEdit::textChanged,
205 this, &FormWidget::slotTextChanged);
206 connect(m_editor, &QTextEdit::selectionChanged,
207 this, &FormWidget::slotSelectionChanged);
208 connect(m_editor, &QTextEdit::cursorPositionChanged,
209 this, &FormWidget::cursorPositionChanged);
210}
211
212void FormWidget::slotTextChanged()
213{
214 emit textChanged(m_editor);
215}
216
217void FormWidget::slotSelectionChanged()
218{
219 emit selectionChanged(m_editor);
220}
221
222void FormWidget::setTranslation(const QString &text, bool userAction)
223{
224 m_editor->setPlainText(text, userAction);
225 if (m_hideWhenEmpty)
226 setHidden(text.isEmpty());
227}
228
230{
231 // Use read-only state so that the text can still be copied
232 m_editor->setReadOnly(!enable);
233 m_label->setEnabled(enable);
234}
235
236
237class ButtonWrapper : public QWidget
238{
239 // no Q_OBJECT: no need to, and don't want the useless moc file
240
241public:
242 ButtonWrapper(QWidget *wrapee, QWidget *relator)
243 {
244 QBoxLayout *box = new QVBoxLayout;
245 box->setContentsMargins(QMargins());
246 setLayout(box);
247 box->addWidget(wrapee, 0, Qt::AlignBottom);
248 if (relator)
249 relator->installEventFilter(this);
250 }
251
252protected:
253 bool eventFilter(QObject *object, QEvent *event) override
254 {
255 if (event->type() == QEvent::Resize) {
256 QWidget *relator = static_cast<QWidget *>(object);
257 setFixedHeight(relator->height());
258 }
259 return false;
260 }
261};
262
263FormMultiWidget::FormMultiWidget(const QString &label, QWidget *parent)
264 : QWidget(parent),
265 m_hideWhenEmpty(false),
266 m_multiEnabled(false)
267{
268 updateIcons();
269 m_label = new QLabel(this);
270 QFont fnt;
271 fnt.setBold(true);
272 m_label->setFont(fnt);
273 m_label->setText(label);
274
275 m_plusButtons.append(
276 new ButtonWrapper(makeButton(m_plusIcon, &FormMultiWidget::plusButtonClicked), 0));
277}
278
279void FormMultiWidget::updateIcons()
280{
281 const QString prefix = isDarkMode() ? ":/images/darkicons"_L1: ":/images/lighticons"_L1;
282 m_plusIcon = QIcon(prefix + "/plus-square-fill.png"_L1);
283 m_minusIcon = QIcon(prefix + "/minus-square-fill.png"_L1);
284 for (QAbstractButton *button: std::as_const(m_minusButtons))
285 button->setIcon(m_minusIcon);
286 for (QWidget *button: std::as_const(m_plusButtons)) {
287 QWidget *w = button->layout()->itemAt(0)->widget();
288 if (auto b = qobject_cast<QAbstractButton*>(w); b)
289 b->setIcon(m_plusIcon);
290 }
291}
292
293QAbstractButton *FormMultiWidget::makeButton(const QIcon &icon)
294{
295 QAbstractButton *btn = new QToolButton(this);
296 btn->setIcon(icon);
297 btn->setFixedSize(icon.availableSizes().first() /* + something */);
298 btn->setFocusPolicy(Qt::NoFocus);
299#ifndef QT_NO_STYLE_STYLESHEET
300 btn->setStyleSheet("border: none; background: transparent;"_L1);
301#endif
302 return btn;
303}
304
305void FormMultiWidget::addEditor(int idx)
306{
307 FormatTextEdit *editor = new FormatTextEdit(this);
308 m_editors.insert(idx, editor);
309
310 m_minusButtons.insert(idx, makeButton(m_minusIcon, &FormMultiWidget::minusButtonClicked));
311 m_plusButtons.insert(idx + 1,
312 new ButtonWrapper(makeButton(m_plusIcon, &FormMultiWidget::plusButtonClicked), editor));
313
314 connect(editor, &QTextEdit::textChanged,
315 this, &FormMultiWidget::slotTextChanged);
316 connect(editor, &QTextEdit::selectionChanged,
317 this, &FormMultiWidget::slotSelectionChanged);
318 connect(editor, &QTextEdit::cursorPositionChanged,
319 this, &FormMultiWidget::cursorPositionChanged);
320 editor->installEventFilter(this);
321
322 emit editorCreated(editor);
323}
324
325bool FormMultiWidget::eventFilter(QObject *watched, QEvent *event)
326{
327 int i = 0;
328 while (m_editors.at(i) != watched)
329 if (++i >= m_editors.size()) // Happens when deleting an editor
330 return false;
331 if (event->type() == QEvent::FocusOut) {
332 m_minusButtons.at(i)->setToolTip(QString());
333 m_plusButtons.at(i)->setToolTip(QString());
334 m_plusButtons.at(i + 1)->setToolTip(QString());
335 } else if (event->type() == QEvent::FocusIn) {
336 m_minusButtons.at(i)->setToolTip(/*: translate, but don't change */ tr("Alt+Delete"));
337 m_plusButtons.at(i)->setToolTip(/*: translate, but don't change */ tr("Shift+Alt+Insert"));
338 m_plusButtons.at(i + 1)->setToolTip(/*: translate, but don't change */ tr("Alt+Insert"));
339 } else if (event->type() == QEvent::KeyPress) {
340 QKeyEvent *ke = static_cast<QKeyEvent *>(event);
341 if (ke->modifiers() & Qt::AltModifier) {
342 if (ke->key() == Qt::Key_Delete) {
343 deleteEditor(i);
344 return true;
345 } else if (ke->key() == Qt::Key_Insert) {
346 if (!(ke->modifiers() & Qt::ShiftModifier))
347 ++i;
348 insertEditor(i);
349 return true;
350 }
351 }
352 } else if (event->type() == QEvent::ApplicationPaletteChange) {
353 updateIcons();
354 }
355 return false;
356}
357
358void FormMultiWidget::updateLayout()
359{
360 delete layout();
361
362 QGridLayout *layout = new QGridLayout;
363 layout->setContentsMargins(QMargins());
364 setLayout(layout);
365
366 bool variants = m_multiEnabled && m_label->isEnabled();
367
368 layout->addWidget(m_label, 0, 0, 1, variants ? 2 : 1);
369
370 if (variants) {
371 QVBoxLayout *layoutForPlusButtons = new QVBoxLayout;
372 layoutForPlusButtons->setContentsMargins(QMargins());
373 for (int i = 0; i < m_plusButtons.size(); ++i)
374 layoutForPlusButtons->addWidget(m_plusButtons.at(i), Qt::AlignTop);
375 layout->addLayout(layoutForPlusButtons, 1, 0, Qt::AlignTop);
376
377 const int minimumRowHeight = m_plusButtons.at(0)->sizeHint().height() / 2.0;
378 QGridLayout *layoutForLabels = new QGridLayout;
379 layoutForLabels->setContentsMargins(QMargins());
380 layoutForLabels->setRowMinimumHeight(0, minimumRowHeight);
381 for (int j = 0; j < m_editors.size(); ++j) {
382 layoutForLabels->addWidget(m_editors.at(j), 1 + j, 0, Qt::AlignVCenter);
383 layoutForLabels->addWidget(m_minusButtons.at(j), 1 + j, 1, Qt::AlignVCenter);
384 }
385 layoutForLabels->setRowMinimumHeight(m_editors.size() + 1, minimumRowHeight);
386 layout->addLayout(layoutForLabels, 1, 1, Qt::AlignTop);
387 } else {
388 for (int k = 0; k < m_editors.size(); ++k)
389 layout->addWidget(m_editors.at(k), 1 + k, 0, Qt::AlignVCenter);
390 }
391
392 for (int i = 0; i < m_plusButtons.size(); ++i)
393 m_plusButtons.at(i)->setVisible(variants);
394 for (int j = 0; j < m_minusButtons.size(); ++j)
395 m_minusButtons.at(j)->setVisible(variants);
396
397 updateGeometry();
398}
399
400void FormMultiWidget::slotTextChanged()
401{
402 emit textChanged(static_cast<QTextEdit *>(sender()));
403}
404
405void FormMultiWidget::slotSelectionChanged()
406{
407 emit selectionChanged(static_cast<QTextEdit *>(sender()));
408}
409
410void FormMultiWidget::setTranslation(const QString &text, bool userAction)
411{
412 QStringList texts = text.split(QChar(Translator::BinaryVariantSeparator), Qt::KeepEmptyParts);
413
414 while (m_editors.size() > texts.size()) {
415 delete m_minusButtons.takeLast();
416 delete m_plusButtons.takeLast();
417 delete m_editors.takeLast();
418 }
419 while (m_editors.size() < texts.size())
420 addEditor(m_editors.size());
421 updateLayout();
422
423 for (int i = 0; i < texts.size(); ++i)
424 // XXX this will emit n textChanged signals
425 m_editors.at(i)->setPlainText(texts.at(i), userAction);
426
427 if (m_hideWhenEmpty)
428 setHidden(text.isEmpty());
429}
430
431// Copied from QTextDocument::toPlainText() and modified to
432// not replace QChar::Nbsp with u' '
433QString toPlainText(const QString &text)
434{
435 QString txt = text;
436 QChar *uc = txt.data();
437 QChar *e = uc + txt.size();
438
439 for (; uc != e; ++uc) {
440 switch (uc->unicode()) {
441 case 0xfdd0: // QTextBeginningOfFrame
442 case 0xfdd1: // QTextEndOfFrame
443 case QChar::ParagraphSeparator:
444 case QChar::LineSeparator:
445 *uc = u'\n';
446 break;
447 }
448 }
449 return txt;
450}
451
453{
454 QString ret;
455 for (int i = 0; i < m_editors.size(); ++i) {
456 if (i)
457 ret += QChar(Translator::BinaryVariantSeparator);
458 ret += toPlainText(m_editors.at(i)->document()->toRawText());
459 }
460 return ret;
461}
462
464{
465 // Use read-only state so that the text can still be copied
466 for (int i = 0; i < m_editors.size(); ++i)
467 m_editors.at(i)->setReadOnly(!enable);
468 m_label->setEnabled(enable);
469 if (m_multiEnabled)
470 updateLayout();
471}
472
474{
475 m_multiEnabled = enable;
476 if (m_label->isEnabled())
477 updateLayout();
478}
479
480void FormMultiWidget::minusButtonClicked()
481{
482 int i = 0;
483 while (m_minusButtons.at(i) != sender())
484 ++i;
485 deleteEditor(i);
486}
487
488void FormMultiWidget::plusButtonClicked()
489{
490 QWidget *btn = static_cast<QAbstractButton *>(sender())->parentWidget();
491 int i = 0;
492 while (m_plusButtons.at(i) != btn)
493 ++i;
494 insertEditor(i);
495}
496
497void FormMultiWidget::deleteEditor(int idx)
498{
499 if (m_editors.size() == 1) {
500 // Don't just clear(), so the undo history is not lost
501 QTextCursor c = m_editors.first()->textCursor();
502 c.select(QTextCursor::Document);
503 c.removeSelectedText();
504 } else {
505 if (!m_editors.at(idx)->toPlainText().isEmpty()) {
506 if (QMessageBox::question(topLevelWidget(), tr("Confirmation - Qt Linguist"),
507 tr("Delete non-empty length variant?"),
508 QMessageBox::Yes|QMessageBox::No, QMessageBox::Yes)
509 != QMessageBox::Yes)
510 return;
511 }
512 delete m_editors.takeAt(idx);
513 delete m_minusButtons.takeAt(idx);
514 delete m_plusButtons.takeAt(idx + 1);
515 updateLayout();
516 emit textChanged(m_editors.at((m_editors.size() == idx) ? idx - 1 : idx));
517 }
518}
519
520void FormMultiWidget::insertEditor(int idx)
521{
522 addEditor(idx);
523 updateLayout();
524 emit textChanged(m_editors.at(idx));
525}
526
527QT_END_NAMESPACE
bool eventFilter(QObject *object, QEvent *event) override
Filters events if this object has been installed as an event filter for the watched object.
ButtonWrapper(QWidget *wrapee, QWidget *relator)
QSize minimumSizeHint() const override
QSize sizeHint() const override
void setTranslation(const QString &text, bool userAction=false)
QString getTranslation() const
bool eventFilter(QObject *watched, QEvent *event) override
Filters events if this object has been installed as an event filter for the watched object.
void setMultiEnabled(bool enable)
void setEditingEnabled(bool enable)
void setTranslation(const QString &text, bool userAction=false)
void setEditingEnabled(bool enable)
void setVisualizeWhitespace(bool value)
bool event(QEvent *event) override
void setEditable(bool editable)
bool isDarkMode()
Definition globals.cpp:38
QString toPlainText(const QString &text)