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

source code of qttools/src/linguist/linguist/messageeditorwidgets.cpp