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 ReplacementValidator(QObject * parent,const QString & offending,const QString & replacement)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 fixup(QString & input) const71 void ReplacementValidator::fixup ( QString & input ) const { 72 input.replace(m_offending, m_replacement); 73 } 74 validate(QString & input,int &) const75 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 StyleSheetValidator(QObject * parent)87 StyleSheetValidator::StyleSheetValidator (QObject * parent) : 88 ReplacementValidator(parent, NewLineChar, EscapedNewLine) 89 { 90 } 91 validate(QString & input,int & pos) const92 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(input, 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 UrlValidator(QCompleter * completer,QObject * parent)117 UrlValidator::UrlValidator(QCompleter *completer, QObject *parent) : 118 QValidator(parent), 119 m_completer(completer) 120 { 121 } 122 validate(QString & input,int & pos) const123 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 fixup(QString & input) const144 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(input).toString(); 153 } 154 guessUrlFromString(const QString & string) const155 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(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(urlStr)) 175 return QUrl::fromLocalFile(urlStr); 176 177 // Might be a short url - try to detect the schema. 178 if (!hasSchema) { 179 const int dotIndex = urlStr.indexOf(QLatin1Char('.')); 180 if (dotIndex != -1) { 181 const QString prefix = urlStr.left(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 TextPropertyEditor(QWidget * parent,EmbeddingMode embeddingMode,TextPropertyValidationMode validationMode)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(m_lineEdit,&QLineEdit::editingFinished, this, &TextPropertyEditor::editingFinished); 224 connect(m_lineEdit,&QLineEdit::returnPressed, this, &TextPropertyEditor::slotEditingFinished); 225 connect(m_lineEdit,&QLineEdit::textChanged, this, &TextPropertyEditor::slotTextChanged); 226 connect(m_lineEdit,&QLineEdit::textEdited, this, &TextPropertyEditor::slotTextEdited); 227 228 setTextPropertyValidationMode(validationMode); 229 } 230 setTextPropertyValidationMode(TextPropertyValidationMode vm)231 void TextPropertyEditor::setTextPropertyValidationMode(TextPropertyValidationMode vm) { 232 m_validationMode = vm; 233 m_lineEdit->setWantNewLine(multiLine(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 setRegularExpressionValidator(const QString & pattern)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 text() const292 QString TextPropertyEditor::text() const 293 { 294 return m_cachedText; 295 } 296 markIntermediateState()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(QPalette::Active, QPalette::Text, Qt::red); 304 m_lineEdit->setPalette(palette); 305 } 306 307 } 308 setText(const QString & text)309 void TextPropertyEditor::setText(const QString &text) 310 { 311 m_cachedText = text; 312 m_lineEdit->setText(stringToEditorString(text, m_validationMode)); 313 markIntermediateState(); 314 m_textEdited = false; 315 } 316 slotTextEdited()317 void TextPropertyEditor::slotTextEdited() 318 { 319 m_textEdited = true; 320 } 321 slotTextChanged(const QString & text)322 void TextPropertyEditor::slotTextChanged(const QString &text) { 323 m_cachedText = editorStringToString(text, m_validationMode); 324 markIntermediateState(); 325 if (m_updateMode == UpdateAsYouType) 326 emit textChanged(m_cachedText); 327 } 328 slotEditingFinished()329 void TextPropertyEditor::slotEditingFinished() 330 { 331 if (m_updateMode == UpdateOnFinished && m_textEdited) { 332 emit textChanged(m_cachedText); 333 m_textEdited = false; 334 } 335 } 336 selectAll()337 void TextPropertyEditor::selectAll() { 338 m_lineEdit->selectAll(); 339 } 340 clear()341 void TextPropertyEditor::clear() { 342 m_lineEdit->clear(); 343 } 344 setAlignment(Qt::Alignment alignment)345 void TextPropertyEditor::setAlignment(Qt::Alignment alignment) { 346 m_lineEdit->setAlignment(alignment); 347 } 348 installEventFilter(QObject * filterObject)349 void TextPropertyEditor::installEventFilter(QObject *filterObject) 350 { 351 if (m_lineEdit) 352 m_lineEdit->installEventFilter(filterObject); 353 } 354 resizeEvent(QResizeEvent * event)355 void TextPropertyEditor::resizeEvent ( QResizeEvent * event ) { 356 m_lineEdit->resize( event->size()); 357 } 358 sizeHint() const359 QSize TextPropertyEditor::sizeHint () const { 360 return m_lineEdit->sizeHint (); 361 } 362 minimumSizeHint() const363 QSize TextPropertyEditor::minimumSizeHint () const { 364 return m_lineEdit->minimumSizeHint (); 365 } 366 367 // Returns whether newline characters are valid in validationMode. multiLine(TextPropertyValidationMode 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 stringToEditorString(const QString & s,TextPropertyValidationMode validationMode)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(QLatin1Char('\\'), QStringLiteral("\\\\")); 380 // escape newlines 381 rc.replace(NewLineChar, 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 editorStringToString(const QString & s,TextPropertyValidationMode validationMode)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(QLatin1Char('\\'),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(nextpos) == QChar(QLatin1Char('n'))) 402 rc[nextpos] = NewLineChar; 403 // Remove escape, go past escaped 404 rc.remove(pos,1); 405 pos++; 406 } 407 return rc; 408 } 409 hasAcceptableInput() const410 bool TextPropertyEditor::hasAcceptableInput() const { 411 return m_lineEdit->hasAcceptableInput(); 412 } 413 } 414 415 QT_END_NAMESPACE 416