From d2bb44479ea87a6bde11c42664a33b24ceda2f1c Mon Sep 17 00:00:00 2001 From: Marc Reilly Date: Fri, 31 Oct 2014 09:57:41 +1100 Subject: [PATCH] texteditor: add a hover handler which shows color preview tooltips This adds a new hover handler which matches stand-alone color strings like "#112233" or "Qt::yellow" or function argument tuples for colors such as "QColor(0x11, 0x22, 0x33)". When matching against function arguments, the function name must correspond to a recognized color function (setRgb, etc. This is biased towards cpp text, but not limited to such). The matching occurs when hovering over the arguments, not the function. If a match is identified, the hover handler gives it a relatively high ranking. Change-Id: Ied2927399cb19d6f562185a8b087f0ce118157db Reviewed-by: David Schulz --- .../texteditor/colorpreviewhoverhandler.cpp | 392 ++++++++++++++++++ .../texteditor/colorpreviewhoverhandler.h | 59 +++ src/plugins/texteditor/texteditor.pro | 2 + 3 files changed, 453 insertions(+) create mode 100644 src/plugins/texteditor/colorpreviewhoverhandler.cpp create mode 100644 src/plugins/texteditor/colorpreviewhoverhandler.h diff --git a/src/plugins/texteditor/colorpreviewhoverhandler.cpp b/src/plugins/texteditor/colorpreviewhoverhandler.cpp new file mode 100644 index 00000000000..f405d24d231 --- /dev/null +++ b/src/plugins/texteditor/colorpreviewhoverhandler.cpp @@ -0,0 +1,392 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://www.qt.io/licensing. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#include "colorpreviewhoverhandler.h" +#include "texteditor.h" + +#include +#include +#include + +#include +#include +#include + +using namespace Core; + +namespace TextEditor { + +/* + * Attempts to find a color string such as "#112233" from the word at + * the given position in the string. Also looks for "Qt::" for recognizing + * Qt::GlobalColor types (although there cannot be any spaces such as + * "Qt:: yellow") + */ +static QString extractColorString(const QString s, int pos) +{ + if (s.length() < 3 || pos < 0 || pos >= s.length()) + return QString(); + + int firstPos = pos; + do { + QChar c = s[firstPos]; + if (c == QLatin1Char('#')) + break; + + if (c == QLatin1Char(':') + && (firstPos > 3) + && (s.mid(firstPos-3, 4) == QLatin1String("Qt::"))) { + firstPos -= 3; + break; + } + + if (!c.isLetterOrNumber()) + return QString(); + + --firstPos; + } while (firstPos >= 0); + + if (firstPos < 0) + return QString(); + + int lastPos = firstPos + 1; + do { + QChar c = s[lastPos]; + if (!(c.isLetterOrNumber() || c == QLatin1Char(':'))) + break; + lastPos++; + } while (lastPos < s.length()); + + return s.mid(firstPos, lastPos - firstPos); +} + +static QColor fromEnumString(const QString &s) +{ + const struct EnumColorMap { + QLatin1String name; + QColor color; + } table[] = { + {QLatin1String("white") , QColor(Qt::white)}, + {QLatin1String("black"), QColor(Qt::black)}, + {QLatin1String("red"), QColor(Qt::red)}, + {QLatin1String("darkRed"), QColor(Qt::darkRed)}, + {QLatin1String("green"), QColor(Qt::green)}, + {QLatin1String("darkGreen"), QColor(Qt::darkGreen)}, + {QLatin1String("blue"), QColor(Qt::blue)}, + {QLatin1String("darkBlue"), QColor(Qt::darkBlue)}, + {QLatin1String("cyan"), QColor(Qt::cyan)}, + {QLatin1String("darkCyan"), QColor(Qt::darkCyan)}, + {QLatin1String("magenta"), QColor(Qt::magenta)}, + {QLatin1String("darkMagenta"), QColor(Qt::darkMagenta)}, + {QLatin1String("yellow"), QColor(Qt::yellow)}, + {QLatin1String("darkYellow"), QColor(Qt::darkYellow)}, + {QLatin1String("gray"), QColor(Qt::gray)}, + {QLatin1String("darkGray"), QColor(Qt::darkGray)}, + {QLatin1String("lightGray"), QColor(Qt::lightGray)}, + {QLatin1String("transparent"), QColor(Qt::transparent)} + }; + + for (uint ii = 0; ii < sizeof(table) / sizeof(table[0]); ++ii) { + if (s == table[ii].name) + return table[ii].color; + } + + return QColor(); +} + +static QColor checkColorText(const QString &str) +{ + if (str.startsWith(QLatin1Char('#'))) + return QColor(str); + + if (str.startsWith(QLatin1String("Qt::"))) { + QString colorStr = str; + colorStr.remove(0, 4); + return fromEnumString(colorStr); + } + + return QColor(); +} + +// looks backwards through a string for the opening brace of a function +static int findOpeningBrace(const QString &s, int startIndex) +{ + QTC_ASSERT(startIndex >= 0 && startIndex < s.length(), return -1); + + int index = startIndex; + while (index > 0) { + const QChar c = s[index]; + if (c == QLatin1Char('(') || c == QLatin1Char('{')) + return index; + + --index; + } + + return index; +} + +static int findClosingBrace(const QString &s, int startIndex) +{ + QTC_ASSERT(startIndex >= 0 && startIndex < s.length(), return -1); + + int index = startIndex; + const int len = s.length(); + while (index < len) { + const QChar c = s[index]; + if (c == QLatin1Char(')') || c == QLatin1Char('}')) + return index; + + ++index; + } + + return -1; +} + +// returns the index of the first character of the func, or negative if not valid +static int findFuncStart(const QString &s, int startIndex) +{ + QTC_ASSERT(startIndex >= 0 && startIndex < s.length(), return -1); + + int index = startIndex; + while (index >= 0) { + const QChar c = s[index]; + if (!c.isLetterOrNumber()) { + if (index == startIndex) + return -1; + + return qMin(index + 1, startIndex); + } + --index; + } + + return index; +} + +static QString removeWhitespace(const QString &s) +{ + QString ret; + ret.reserve(s.size()); + for (int ii = 0; ii < s.length(); ++ii) { + const QChar c = s[ii]; + if (!c.isSpace()) + ret += c; + + } + return ret; +} + +/* + * Parses the string looking for a function and its arguments. + * The starting position is assumed to be within the braces. + */ +static bool extractFuncAndArgs(const QString &s, + int pos, + QString &retFuncName, + QStringList &retArgs) +{ + int openBrace = findOpeningBrace(s, pos); + if (openBrace <= 0) + return false; + + int closeBrace = findClosingBrace(s, openBrace + 1); + if (closeBrace < 0) + return false; + + int funcEnd = openBrace - 1; + int funcStart = findFuncStart(s, funcEnd); + + if (funcStart < 0 || funcEnd <= funcStart) + return false; + + retFuncName = removeWhitespace(s.mid(funcStart, funcEnd - funcStart + 1)); + + QString argStr = s.mid(openBrace + 1, closeBrace - openBrace - 1); + retArgs = argStr.split(QLatin1Char(','), QString::KeepEmptyParts); + + return true; +} + +static QColor::Spec specForFunc(const QString &func) +{ + if ((func == QLatin1String("QColor")) + || (func == QLatin1String("QRgb")) + || (func == QLatin1String("rgb")) + || func.startsWith(QLatin1String("setRgb")) + || func.startsWith(QLatin1String("setRgba"))){ + return QColor::Rgb; + } + + if (func.startsWith(QLatin1String("setCmyk"))) + return QColor::Cmyk; + + if (func.startsWith(QLatin1String("setHsv"))) + return QColor::Hsv; + + if (func.startsWith(QLatin1String("setHsl"))) + return QColor::Hsv; + + return QColor::Invalid; +} + +static QColor colorFromArgs(const QStringList &args, QColor::Spec spec) +{ + const int maxArgs = 5; + int vals[maxArgs]; + vals[3] = 0xff; + vals[4] = 0xff; + + bool allOk = true; + for (int ii = 0; ii < qMin(args.size(), maxArgs); ++ii) { + bool ok; + vals[ii] = args[ii].toInt(&ok, 0); + allOk &= ok; + } + + if (!allOk) + return QColor(); + + QColor c; + switch (spec) { + case QColor::Rgb: + c.setRgb(vals[0], vals[1], vals[2], vals[3]); + break; + case QColor::Cmyk: + c.setCmyk(vals[0], vals[1], vals[2], vals[3], vals[4]); + break; + case QColor::Hsv: + c.setHsv(vals[0], vals[1], vals[2], vals[3]); + break; + case QColor::Hsl: + c.setHsl(vals[0], vals[1], vals[2], vals[3]); + break; + default: + break; + } + return c; +} + +static QColor colorFromArgsF(const QStringList &args, QColor::Spec spec) +{ + const int maxArgs = 5; + qreal vals[maxArgs]; + vals[3] = 1.0; + vals[4] = 1.0; + + bool allOk = true; + for (int ii = 0; ii < qMin(args.size(), maxArgs); ++ii) { + bool ok; + vals[ii] = args[ii].toDouble(&ok); + allOk &= ok; + } + if (!allOk) + return QColor(); + + QColor c; + switch (spec) { + case QColor::Rgb: + c.setRgbF(vals[0], vals[1], vals[2], vals[3]); + break; + case QColor::Cmyk: + c.setCmykF(vals[0], vals[1], vals[2], vals[3], vals[4]); + break; + case QColor::Hsv: + c.setHsvF(vals[0], vals[1], vals[2], vals[3]); + break; + case QColor::Hsl: + c.setHslF(vals[0], vals[1], vals[2], vals[3]); + break; + default: + break; + } + return c; +} + +static QColor colorFromFuncAndArgs(const QString &func, const QStringList &args) +{ + if (args.isEmpty()) + return QColor(); + + if (args.size() < 3) { + QString arg0 = removeWhitespace(args[0]); + arg0.remove(QLatin1Char('\"')); + if (func == QLatin1String("setNamedColor")) + return QColor(arg0); + + if (arg0.startsWith(QLatin1Char('#'))) + return QColor(arg0); + + if (arg0.startsWith(QLatin1String("Qt::"))) { + arg0.remove(0, 4); + return fromEnumString(arg0); + } + + return QColor(); + } + + QColor::Spec spec = specForFunc(func); + if (spec == QColor::Invalid) + return QColor(); + + if (func.endsWith(QLatin1Char('F'))) + return colorFromArgsF(args, spec); + + return colorFromArgs(args, spec); +} + +void ColorPreviewHoverHandler::identifyMatch(TextEditorWidget *editorWidget, int pos) +{ + if (editorWidget->extraSelectionTooltip(pos).isEmpty()) { + const QTextBlock tb = editorWidget->document()->findBlock(pos); + const int tbpos = pos - tb.position(); + const QString tbtext = tb.text(); + + QString colorString = extractColorString(tbtext, tbpos); + m_colorTip = checkColorText(colorString); + + if (!m_colorTip.isValid()) { + QString funcName; + QStringList args; + if (extractFuncAndArgs(tbtext, tbpos, funcName, args)) + m_colorTip = colorFromFuncAndArgs(funcName, args); + } + + setPriority(m_colorTip.isValid() ? Priority_Help - 1 : Priority_None); + } +} + +void ColorPreviewHoverHandler::operateTooltip(TextEditorWidget *editorWidget, const QPoint &point) +{ + if (m_colorTip.isValid()) + Utils::ToolTip::show(point, m_colorTip, editorWidget); + else + Utils::ToolTip::hide(); +} + +} // namespace TextEditor diff --git a/src/plugins/texteditor/colorpreviewhoverhandler.h b/src/plugins/texteditor/colorpreviewhoverhandler.h new file mode 100644 index 00000000000..abb001dcef3 --- /dev/null +++ b/src/plugins/texteditor/colorpreviewhoverhandler.h @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://www.qt.io/licensing. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#ifndef COLORPREVIEWHOVERHANDLER_H +#define COLORPREVIEWHOVERHANDLER_H + +#include "texteditor_global.h" +#include "basehoverhandler.h" + +#include + +namespace Core { class IEditor; } + +namespace TextEditor { + +class TextEditorWidget; + +class TEXTEDITOR_EXPORT ColorPreviewHoverHandler : public BaseHoverHandler +{ + Q_OBJECT +public: + +private: + virtual void identifyMatch(TextEditorWidget *editorWidget, int pos); + virtual void operateTooltip(TextEditorWidget *editorWidget, const QPoint &point); + + QColor m_colorTip; +}; + +} // namespace TextEditor + +#endif // COLORPREVIEWHOVERHANDLER_H diff --git a/src/plugins/texteditor/texteditor.pro b/src/plugins/texteditor/texteditor.pro index 9c6d2c0919e..d65ba317ca9 100644 --- a/src/plugins/texteditor/texteditor.pro +++ b/src/plugins/texteditor/texteditor.pro @@ -55,6 +55,7 @@ SOURCES += texteditorplugin.cpp \ refactoroverlay.cpp \ outlinefactory.cpp \ basehoverhandler.cpp \ + colorpreviewhoverhandler.cpp \ helpitem.cpp \ autocompleter.cpp \ snippets/snippetssettingspage.cpp \ @@ -161,6 +162,7 @@ HEADERS += texteditorplugin.h \ outlinefactory.h \ ioutlinewidget.h \ basehoverhandler.h \ + colorpreviewhoverhandler.h \ helpitem.h \ autocompleter.h \ snippets/snippetssettingspage.h \