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