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 | |
50 | QT_BEGIN_NAMESPACE |
51 | |
52 | ExpandingTextEdit::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 | |
67 | void ExpandingTextEdit::updateHeight(const QSizeF &documentSize) |
68 | { |
69 | m_minimumHeight = qRound(d: documentSize.height()) + frameWidth() * 2; |
70 | updateGeometry(); |
71 | } |
72 | |
73 | QSize ExpandingTextEdit::sizeHint() const |
74 | { |
75 | return QSize(100, m_minimumHeight); |
76 | } |
77 | |
78 | QSize ExpandingTextEdit::minimumSizeHint() const |
79 | { |
80 | return QSize(100, m_minimumHeight); |
81 | } |
82 | |
83 | void 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 | |
100 | FormatTextEdit::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 | |
116 | FormatTextEdit::~FormatTextEdit() |
117 | { |
118 | emit editorDestroyed(); |
119 | } |
120 | |
121 | void 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 | |
138 | void 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 | |
154 | void 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 | |
169 | FormWidget::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 | |
195 | void FormWidget::slotTextChanged() |
196 | { |
197 | emit textChanged(m_editor); |
198 | } |
199 | |
200 | void FormWidget::slotSelectionChanged() |
201 | { |
202 | emit selectionChanged(m_editor); |
203 | } |
204 | |
205 | void FormWidget::setTranslation(const QString &text, bool userAction) |
206 | { |
207 | m_editor->setPlainText(text, userAction); |
208 | if (m_hideWhenEmpty) |
209 | setHidden(text.isEmpty()); |
210 | } |
211 | |
212 | void 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 | |
220 | class ButtonWrapper : public QWidget |
221 | { |
222 | // no Q_OBJECT: no need to, and don't want the useless moc file |
223 | |
224 | public: |
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 | |
235 | protected: |
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 | |
246 | FormMultiWidget::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 | |
263 | QAbstractButton *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 | |
273 | void 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 | |
290 | bool 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 | |
321 | void 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 | |
363 | void FormMultiWidget::slotTextChanged() |
364 | { |
365 | emit textChanged(static_cast<QTextEdit *>(sender())); |
366 | } |
367 | |
368 | void FormMultiWidget::slotSelectionChanged() |
369 | { |
370 | emit selectionChanged(static_cast<QTextEdit *>(sender())); |
371 | } |
372 | |
373 | void 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(' ') |
396 | QString 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 | |
415 | QString 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 | |
426 | void 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 | |
436 | void FormMultiWidget::setMultiEnabled(bool enable) |
437 | { |
438 | m_multiEnabled = enable; |
439 | if (m_label->isEnabled()) |
440 | updateLayout(); |
441 | } |
442 | |
443 | void FormMultiWidget::minusButtonClicked() |
444 | { |
445 | int i = 0; |
446 | while (m_minusButtons.at(i) != sender()) |
447 | ++i; |
448 | deleteEditor(idx: i); |
449 | } |
450 | |
451 | void 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 | |
460 | void 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 | |
483 | void FormMultiWidget::insertEditor(int idx) |
484 | { |
485 | addEditor(idx); |
486 | updateLayout(); |
487 | emit textChanged(m_editors.at(i: idx)); |
488 | } |
489 | |
490 | QT_END_NAMESPACE |
491 | |