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

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