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 | |
25 | QT_BEGIN_NAMESPACE |
26 | |
27 | ExpandingTextEdit::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 | |
44 | void ExpandingTextEdit::updateHeight(const QSizeF &documentSize) |
45 | { |
46 | m_minimumHeight = qRound(d: documentSize.height()) + frameWidth() * 2; |
47 | updateGeometry(); |
48 | } |
49 | |
50 | QSize ExpandingTextEdit::sizeHint() const |
51 | { |
52 | return QSize(100, m_minimumHeight); |
53 | } |
54 | |
55 | QSize ExpandingTextEdit::minimumSizeHint() const |
56 | { |
57 | return QSize(100, m_minimumHeight); |
58 | } |
59 | |
60 | void 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 | |
77 | FormatTextEdit::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 | |
93 | FormatTextEdit::~FormatTextEdit() |
94 | { |
95 | emit editorDestroyed(); |
96 | } |
97 | |
98 | void 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 | |
115 | void 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 | |
131 | void 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 | |
146 | FormWidget::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 | |
175 | void FormWidget::slotTextChanged() |
176 | { |
177 | emit textChanged(m_editor); |
178 | } |
179 | |
180 | void FormWidget::slotSelectionChanged() |
181 | { |
182 | emit selectionChanged(m_editor); |
183 | } |
184 | |
185 | void FormWidget::setTranslation(const QString &text, bool userAction) |
186 | { |
187 | m_editor->setPlainText(text, userAction); |
188 | if (m_hideWhenEmpty) |
189 | setHidden(text.isEmpty()); |
190 | } |
191 | |
192 | void 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 | |
200 | class ButtonWrapper : public QWidget |
201 | { |
202 | // no Q_OBJECT: no need to, and don't want the useless moc file |
203 | |
204 | public: |
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 | |
215 | protected: |
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 | |
226 | FormMultiWidget::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 | |
243 | QAbstractButton *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 | |
252 | void 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 | |
272 | bool 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 | |
303 | void 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 | |
345 | void FormMultiWidget::slotTextChanged() |
346 | { |
347 | emit textChanged(static_cast<QTextEdit *>(sender())); |
348 | } |
349 | |
350 | void FormMultiWidget::slotSelectionChanged() |
351 | { |
352 | emit selectionChanged(static_cast<QTextEdit *>(sender())); |
353 | } |
354 | |
355 | void 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(' ') |
378 | QString 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 | |
397 | QString 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 | |
408 | void 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 | |
418 | void FormMultiWidget::setMultiEnabled(bool enable) |
419 | { |
420 | m_multiEnabled = enable; |
421 | if (m_label->isEnabled()) |
422 | updateLayout(); |
423 | } |
424 | |
425 | void FormMultiWidget::minusButtonClicked() |
426 | { |
427 | int i = 0; |
428 | while (m_minusButtons.at(i) != sender()) |
429 | ++i; |
430 | deleteEditor(idx: i); |
431 | } |
432 | |
433 | void 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 | |
442 | void 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 | |
465 | void FormMultiWidget::insertEditor(int idx) |
466 | { |
467 | addEditor(idx); |
468 | updateLayout(); |
469 | emit textChanged(m_editors.at(i: idx)); |
470 | } |
471 | |
472 | QT_END_NAMESPACE |
473 | |