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 Designer 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 "textpropertyeditor_p.h" |
30 | #include "propertylineedit_p.h" |
31 | #include "stylesheeteditor_p.h" |
32 | |
33 | #include <QtWidgets/qlineedit.h> |
34 | #include <QtGui/qvalidator.h> |
35 | #include <QtGui/qevent.h> |
36 | #include <QtWidgets/qcompleter.h> |
37 | #include <QtWidgets/qabstractitemview.h> |
38 | #include <QtCore/qregularexpression.h> |
39 | #include <QtCore/qurl.h> |
40 | #include <QtCore/qfile.h> |
41 | #include <QtCore/qdebug.h> |
42 | |
43 | QT_BEGIN_NAMESPACE |
44 | |
45 | namespace { |
46 | const QChar NewLineChar(QLatin1Char('\n')); |
47 | const QLatin1String EscapedNewLine("\\n" ); |
48 | |
49 | // A validator that replaces offending strings |
50 | class ReplacementValidator : public QValidator { |
51 | public: |
52 | ReplacementValidator (QObject * parent, |
53 | const QString &offending, |
54 | const QString &replacement); |
55 | void fixup ( QString & input ) const override; |
56 | State validate ( QString & input, int &pos) const override; |
57 | private: |
58 | const QString m_offending; |
59 | const QString m_replacement; |
60 | }; |
61 | |
62 | ReplacementValidator::ReplacementValidator (QObject * parent, |
63 | const QString &offending, |
64 | const QString &replacement) : |
65 | QValidator(parent ), |
66 | m_offending(offending), |
67 | m_replacement(replacement) |
68 | { |
69 | } |
70 | |
71 | void ReplacementValidator::fixup ( QString & input ) const { |
72 | input.replace(before: m_offending, after: m_replacement); |
73 | } |
74 | |
75 | QValidator::State ReplacementValidator::validate ( QString & input, int &/* pos */) const { |
76 | fixup (input); |
77 | return Acceptable; |
78 | } |
79 | |
80 | // A validator for style sheets. Does newline handling and validates sheets. |
81 | class StyleSheetValidator : public ReplacementValidator { |
82 | public: |
83 | StyleSheetValidator (QObject * parent); |
84 | State validate(QString & input, int &pos) const override; |
85 | }; |
86 | |
87 | StyleSheetValidator::StyleSheetValidator (QObject * parent) : |
88 | ReplacementValidator(parent, NewLineChar, EscapedNewLine) |
89 | { |
90 | } |
91 | |
92 | QValidator::State StyleSheetValidator::validate ( QString & input, int &pos) const |
93 | { |
94 | // base class |
95 | const State state = ReplacementValidator:: validate(input, pos); |
96 | if (state != Acceptable) |
97 | return state; |
98 | // now check style sheet, create string with newlines |
99 | const QString styleSheet = qdesigner_internal::TextPropertyEditor::editorStringToString(s: input, validationMode: qdesigner_internal::ValidationStyleSheet); |
100 | const bool valid = qdesigner_internal::StyleSheetEditorDialog::isStyleSheetValid(styleSheet); |
101 | return valid ? Acceptable : Intermediate; |
102 | } |
103 | |
104 | // A validator for URLs based on QUrl. Enforces complete protocol |
105 | // specification with a completer (adds a trailing slash) |
106 | class UrlValidator : public QValidator { |
107 | public: |
108 | UrlValidator(QCompleter *completer, QObject *parent); |
109 | |
110 | State validate(QString &input, int &pos) const override; |
111 | void fixup(QString &input) const override; |
112 | private: |
113 | QUrl guessUrlFromString(const QString &string) const; |
114 | QCompleter *m_completer; |
115 | }; |
116 | |
117 | UrlValidator::UrlValidator(QCompleter *completer, QObject *parent) : |
118 | QValidator(parent), |
119 | m_completer(completer) |
120 | { |
121 | } |
122 | |
123 | QValidator::State UrlValidator::validate(QString &input, int &pos) const |
124 | { |
125 | Q_UNUSED(pos); |
126 | |
127 | if (input.isEmpty()) |
128 | return Acceptable; |
129 | |
130 | const QUrl url(input, QUrl::StrictMode); |
131 | |
132 | if (!url.isValid() || url.isEmpty()) |
133 | return Intermediate; |
134 | |
135 | if (url.scheme().isEmpty()) |
136 | return Intermediate; |
137 | |
138 | if (url.host().isEmpty() && url.path().isEmpty()) |
139 | return Intermediate; |
140 | |
141 | return Acceptable; |
142 | } |
143 | |
144 | void UrlValidator::fixup(QString &input) const |
145 | { |
146 | // Don't try to fixup if the user is busy selecting a completion proposal |
147 | if (const QAbstractItemView *iv = m_completer->popup()) { |
148 | if (iv->isVisible()) |
149 | return; |
150 | } |
151 | |
152 | input = guessUrlFromString(string: input).toString(); |
153 | } |
154 | |
155 | QUrl UrlValidator::guessUrlFromString(const QString &string) const |
156 | { |
157 | const QString urlStr = string.trimmed(); |
158 | const QRegularExpression qualifiedUrl(QStringLiteral("^[a-zA-Z]+\\:.*$" )); |
159 | Q_ASSERT(qualifiedUrl.isValid()); |
160 | |
161 | // Check if it looks like a qualified URL. Try parsing it and see. |
162 | const bool hasSchema = qualifiedUrl.match(subject: urlStr).hasMatch(); |
163 | if (hasSchema) { |
164 | const QUrl url(urlStr, QUrl::TolerantMode); |
165 | if (url.isValid()) |
166 | return url; |
167 | } |
168 | |
169 | // Might be a Qt resource |
170 | if (string.startsWith(QStringLiteral(":/" ))) |
171 | return QUrl(QStringLiteral("qrc" ) + string); |
172 | |
173 | // Might be a file. |
174 | if (QFile::exists(fileName: urlStr)) |
175 | return QUrl::fromLocalFile(localfile: urlStr); |
176 | |
177 | // Might be a short url - try to detect the schema. |
178 | if (!hasSchema) { |
179 | const int dotIndex = urlStr.indexOf(c: QLatin1Char('.')); |
180 | if (dotIndex != -1) { |
181 | const QString prefix = urlStr.left(n: dotIndex).toLower(); |
182 | QString urlString; |
183 | if (prefix == QStringLiteral("ftp" )) |
184 | urlString += prefix; |
185 | else |
186 | urlString += QStringLiteral("http" ); |
187 | urlString += QStringLiteral("://" ); |
188 | urlString += urlStr; |
189 | const QUrl url(urlString, QUrl::TolerantMode); |
190 | if (url.isValid()) |
191 | return url; |
192 | } |
193 | } |
194 | |
195 | // Fall back to QUrl's own tolerant parser. |
196 | return QUrl(string, QUrl::TolerantMode); |
197 | } |
198 | } |
199 | |
200 | namespace qdesigner_internal { |
201 | // TextPropertyEditor |
202 | TextPropertyEditor::TextPropertyEditor(QWidget *parent, |
203 | EmbeddingMode embeddingMode, |
204 | TextPropertyValidationMode validationMode) : |
205 | QWidget(parent), |
206 | m_lineEdit(new PropertyLineEdit(this)) |
207 | { |
208 | switch (embeddingMode) { |
209 | case EmbeddingNone: |
210 | break; |
211 | case EmbeddingTreeView: |
212 | m_lineEdit->setFrame(false); |
213 | break; |
214 | case EmbeddingInPlace: |
215 | m_lineEdit->setFrame(false); |
216 | Q_ASSERT(parent); |
217 | m_lineEdit->setBackgroundRole(parent->backgroundRole()); |
218 | break; |
219 | } |
220 | |
221 | setFocusProxy(m_lineEdit); |
222 | |
223 | connect(sender: m_lineEdit,signal: &QLineEdit::editingFinished, receiver: this, slot: &TextPropertyEditor::editingFinished); |
224 | connect(sender: m_lineEdit,signal: &QLineEdit::returnPressed, receiver: this, slot: &TextPropertyEditor::slotEditingFinished); |
225 | connect(sender: m_lineEdit,signal: &QLineEdit::textChanged, receiver: this, slot: &TextPropertyEditor::slotTextChanged); |
226 | connect(sender: m_lineEdit,signal: &QLineEdit::textEdited, receiver: this, slot: &TextPropertyEditor::slotTextEdited); |
227 | |
228 | setTextPropertyValidationMode(validationMode); |
229 | } |
230 | |
231 | void TextPropertyEditor::setTextPropertyValidationMode(TextPropertyValidationMode vm) { |
232 | m_validationMode = vm; |
233 | m_lineEdit->setWantNewLine(multiLine(validationMode: m_validationMode)); |
234 | switch (m_validationMode) { |
235 | case ValidationStyleSheet: |
236 | m_lineEdit->setValidator(new StyleSheetValidator(m_lineEdit)); |
237 | m_lineEdit->setCompleter(nullptr); |
238 | break; |
239 | case ValidationMultiLine: |
240 | case ValidationRichText: |
241 | // Set a validator that replaces newline characters by literal "\\n". |
242 | // While it is not possible to actually type a newline characters, |
243 | // it can be pasted into the line edit. |
244 | m_lineEdit->setValidator(new ReplacementValidator(m_lineEdit, NewLineChar, EscapedNewLine)); |
245 | m_lineEdit->setCompleter(nullptr); |
246 | break; |
247 | case ValidationSingleLine: |
248 | // Set a validator that replaces newline characters by a blank. |
249 | m_lineEdit->setValidator(new ReplacementValidator(m_lineEdit, NewLineChar, QString(QLatin1Char(' ')))); |
250 | m_lineEdit->setCompleter(nullptr); |
251 | break; |
252 | case ValidationObjectName: |
253 | setRegularExpressionValidator(QStringLiteral("^[_a-zA-Z][_a-zA-Z0-9]{1,1023}$" )); |
254 | m_lineEdit->setCompleter(nullptr); |
255 | break; |
256 | case ValidationObjectNameScope: |
257 | setRegularExpressionValidator(QStringLiteral("^[_a-zA-Z:][_a-zA-Z0-9:]{1,1023}$" )); |
258 | m_lineEdit->setCompleter(nullptr); |
259 | break; |
260 | case ValidationURL: { |
261 | static QStringList urlCompletions; |
262 | if (urlCompletions.isEmpty()) { |
263 | urlCompletions.push_back(QStringLiteral("about:blank" )); |
264 | urlCompletions.push_back(QStringLiteral("http://" )); |
265 | urlCompletions.push_back(QStringLiteral("http://www." )); |
266 | urlCompletions.push_back(QStringLiteral("http://qt.io" )); |
267 | urlCompletions.push_back(QStringLiteral("file://" )); |
268 | urlCompletions.push_back(QStringLiteral("ftp://" )); |
269 | urlCompletions.push_back(QStringLiteral("data:" )); |
270 | urlCompletions.push_back(QStringLiteral("data:text/html," )); |
271 | urlCompletions.push_back(QStringLiteral("qrc:/" )); |
272 | } |
273 | QCompleter *completer = new QCompleter(urlCompletions, m_lineEdit); |
274 | m_lineEdit->setCompleter(completer); |
275 | m_lineEdit->setValidator(new UrlValidator(completer, m_lineEdit)); |
276 | } |
277 | break; |
278 | } |
279 | |
280 | setFocusProxy(m_lineEdit); |
281 | setText(m_cachedText); |
282 | markIntermediateState(); |
283 | } |
284 | |
285 | void TextPropertyEditor::setRegularExpressionValidator(const QString &pattern) |
286 | { |
287 | QRegularExpression regExp(pattern); |
288 | Q_ASSERT(regExp.isValid()); |
289 | m_lineEdit->setValidator(new QRegularExpressionValidator(regExp, m_lineEdit)); |
290 | } |
291 | |
292 | QString TextPropertyEditor::text() const |
293 | { |
294 | return m_cachedText; |
295 | } |
296 | |
297 | void TextPropertyEditor::markIntermediateState() |
298 | { |
299 | if (m_lineEdit->hasAcceptableInput()) { |
300 | m_lineEdit->setPalette(QPalette()); |
301 | } else { |
302 | QPalette palette = m_lineEdit->palette(); |
303 | palette.setColor(acg: QPalette::Active, acr: QPalette::Text, acolor: Qt::red); |
304 | m_lineEdit->setPalette(palette); |
305 | } |
306 | |
307 | } |
308 | |
309 | void TextPropertyEditor::setText(const QString &text) |
310 | { |
311 | m_cachedText = text; |
312 | m_lineEdit->setText(stringToEditorString(s: text, validationMode: m_validationMode)); |
313 | markIntermediateState(); |
314 | m_textEdited = false; |
315 | } |
316 | |
317 | void TextPropertyEditor::slotTextEdited() |
318 | { |
319 | m_textEdited = true; |
320 | } |
321 | |
322 | void TextPropertyEditor::slotTextChanged(const QString &text) { |
323 | m_cachedText = editorStringToString(s: text, validationMode: m_validationMode); |
324 | markIntermediateState(); |
325 | if (m_updateMode == UpdateAsYouType) |
326 | emit textChanged(text: m_cachedText); |
327 | } |
328 | |
329 | void TextPropertyEditor::slotEditingFinished() |
330 | { |
331 | if (m_updateMode == UpdateOnFinished && m_textEdited) { |
332 | emit textChanged(text: m_cachedText); |
333 | m_textEdited = false; |
334 | } |
335 | } |
336 | |
337 | void TextPropertyEditor::selectAll() { |
338 | m_lineEdit->selectAll(); |
339 | } |
340 | |
341 | void TextPropertyEditor::clear() { |
342 | m_lineEdit->clear(); |
343 | } |
344 | |
345 | void TextPropertyEditor::setAlignment(Qt::Alignment alignment) { |
346 | m_lineEdit->setAlignment(alignment); |
347 | } |
348 | |
349 | void TextPropertyEditor::installEventFilter(QObject *filterObject) |
350 | { |
351 | if (m_lineEdit) |
352 | m_lineEdit->installEventFilter(filterObj: filterObject); |
353 | } |
354 | |
355 | void TextPropertyEditor::resizeEvent ( QResizeEvent * event ) { |
356 | m_lineEdit->resize( event->size()); |
357 | } |
358 | |
359 | QSize TextPropertyEditor::sizeHint () const { |
360 | return m_lineEdit->sizeHint (); |
361 | } |
362 | |
363 | QSize TextPropertyEditor::minimumSizeHint () const { |
364 | return m_lineEdit->minimumSizeHint (); |
365 | } |
366 | |
367 | // Returns whether newline characters are valid in validationMode. |
368 | bool TextPropertyEditor::multiLine(TextPropertyValidationMode validationMode) { |
369 | return validationMode == ValidationMultiLine || validationMode == ValidationStyleSheet || validationMode == ValidationRichText; |
370 | } |
371 | |
372 | // Replace newline characters literal "\n" for inline editing in mode ValidationMultiLine |
373 | QString TextPropertyEditor::stringToEditorString(const QString &s, TextPropertyValidationMode validationMode) { |
374 | if (s.isEmpty() || !multiLine(validationMode)) |
375 | return s; |
376 | |
377 | QString rc(s); |
378 | // protect backslashes |
379 | rc.replace(c: QLatin1Char('\\'), QStringLiteral("\\\\" )); |
380 | // escape newlines |
381 | rc.replace(c: NewLineChar, after: QString(EscapedNewLine)); |
382 | return rc; |
383 | |
384 | } |
385 | |
386 | // Replace literal "\n" by actual new lines for inline editing in mode ValidationMultiLine |
387 | // Note: As the properties are updated while the user types, it is important |
388 | // that trailing slashes ('bla\') are not deleted nor ignored, else this will |
389 | // cause jumping of the cursor |
390 | QString TextPropertyEditor::editorStringToString(const QString &s, TextPropertyValidationMode validationMode) { |
391 | if (s.isEmpty() || !multiLine(validationMode)) |
392 | return s; |
393 | |
394 | QString rc(s); |
395 | for (int pos = 0; (pos = rc.indexOf(c: QLatin1Char('\\'),from: pos)) >= 0 ; ) { |
396 | // found an escaped character. If not a newline or at end of string, leave as is, else insert '\n' |
397 | const int nextpos = pos + 1; |
398 | if (nextpos >= rc.length()) // trailing '\\' |
399 | break; |
400 | // Escaped NewLine |
401 | if (rc.at(i: nextpos) == QChar(QLatin1Char('n'))) |
402 | rc[nextpos] = NewLineChar; |
403 | // Remove escape, go past escaped |
404 | rc.remove(i: pos,len: 1); |
405 | pos++; |
406 | } |
407 | return rc; |
408 | } |
409 | |
410 | bool TextPropertyEditor::hasAcceptableInput() const { |
411 | return m_lineEdit->hasAcceptableInput(); |
412 | } |
413 | } |
414 | |
415 | QT_END_NAMESPACE |
416 | |