1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of Qt Creator.
7**
8** Commercial License Usage
9** Licensees holding valid commercial Qt licenses may use this file in
10** accordance with the commercial license agreement provided with the
11** Software or, alternatively, in accordance with the terms contained in
12** a written agreement between you and The Qt Company. For licensing terms
13** and conditions see https://www.qt.io/terms-conditions. For further
14** information use the contact form at https://www.qt.io/contact-us.
15**
16** GNU General Public License Usage
17** Alternatively, this file may be used under the terms of the GNU
18** General Public License version 3 as published by the Free Software
19** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20** included in the packaging of this file. Please review the following
21** information to ensure the GNU General Public License requirements will
22** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23**
24****************************************************************************/
25
26#include "textmark.h"
27#include "textdocument.h"
28#include "texteditor.h"
29#include "texteditorplugin.h"
30
31#include <coreplugin/editormanager/editormanager.h>
32#include <coreplugin/documentmanager.h>
33#include <utils/qtcassert.h>
34#include <utils/tooltip/tooltip.h>
35
36#include <QAction>
37#include <QGridLayout>
38#include <QPainter>
39#include <QToolButton>
40
41using namespace Core;
42using namespace Utils;
43using namespace TextEditor::Internal;
44
45namespace TextEditor {
46
47class TextMarkRegistry : public QObject
48{
49 Q_OBJECT
50public:
51 static void add(TextMark *mark);
52 static bool remove(TextMark *mark);
53
54private:
55 TextMarkRegistry(QObject *parent);
56 static TextMarkRegistry* instance();
57 void editorOpened(Core::IEditor *editor);
58 void documentRenamed(Core::IDocument *document, const QString &oldName, const QString &newName);
59 void allDocumentsRenamed(const QString &oldName, const QString &newName);
60
61 QHash<Utils::FilePath, QSet<TextMark *> > m_marks;
62};
63
64class AnnotationColors
65{
66public:
67 static AnnotationColors &getAnnotationColors(const QColor &markColor,
68 const QColor &backgroundColor);
69
70public:
71 using SourceColors = QPair<QColor, QColor>;
72 QColor rectColor;
73 QColor textColor;
74
75private:
76 static QHash<SourceColors, AnnotationColors> m_colorCache;
77};
78
79TextMarkRegistry *m_instance = nullptr;
80
81TextMark::TextMark(const FilePath &fileName, int lineNumber, Id category, double widthFactor)
82 : m_fileName(fileName)
83 , m_lineNumber(lineNumber)
84 , m_visible(true)
85 , m_category(category)
86 , m_widthFactor(widthFactor)
87{
88 if (!m_fileName.isEmpty())
89 TextMarkRegistry::add(this);
90}
91
92TextMark::~TextMark()
93{
94 qDeleteAll(m_actions);
95 m_actions.clear();
96 if (!m_fileName.isEmpty())
97 TextMarkRegistry::remove(this);
98 if (m_baseTextDocument)
99 m_baseTextDocument->removeMark(this);
100 m_baseTextDocument = nullptr;
101}
102
103FilePath TextMark::fileName() const
104{
105 return m_fileName;
106}
107
108void TextMark::updateFileName(const FilePath &fileName)
109{
110 if (fileName == m_fileName)
111 return;
112 if (!m_fileName.isEmpty())
113 TextMarkRegistry::remove(this);
114 m_fileName = fileName;
115 if (!m_fileName.isEmpty())
116 TextMarkRegistry::add(this);
117}
118
119int TextMark::lineNumber() const
120{
121 return m_lineNumber;
122}
123
124void TextMark::paintIcon(QPainter *painter, const QRect &rect) const
125{
126 m_icon.paint(painter, rect, Qt::AlignCenter);
127}
128
129void TextMark::paintAnnotation(QPainter &painter, QRectF *annotationRect,
130 const qreal fadeInOffset, const qreal fadeOutOffset,
131 const QPointF &contentOffset) const
132{
133 QString text = lineAnnotation();
134 if (text.isEmpty())
135 return;
136
137 const AnnotationRects &rects = annotationRects(*annotationRect, painter.fontMetrics(),
138 fadeInOffset, fadeOutOffset);
139 const QColor &markColor = m_hasColor ? Utils::creatorTheme()->color(m_color).toHsl()
140 : painter.pen().color();
141 const AnnotationColors &colors = AnnotationColors::getAnnotationColors(
142 markColor, painter.background().color());
143
144 painter.save();
145 QLinearGradient grad(rects.fadeInRect.topLeft() - contentOffset,
146 rects.fadeInRect.topRight() - contentOffset);
147 grad.setColorAt(0.0, Qt::transparent);
148 grad.setColorAt(1.0, colors.rectColor);
149 painter.fillRect(rects.fadeInRect, grad);
150 painter.fillRect(rects.annotationRect, colors.rectColor);
151 painter.setPen(colors.textColor);
152 paintIcon(&painter, rects.iconRect.toAlignedRect());
153 painter.drawText(rects.textRect, Qt::AlignLeft, rects.text);
154 if (rects.fadeOutRect.isValid()) {
155 grad = QLinearGradient(rects.fadeOutRect.topLeft() - contentOffset,
156 rects.fadeOutRect.topRight() - contentOffset);
157 grad.setColorAt(0.0, colors.rectColor);
158 grad.setColorAt(1.0, Qt::transparent);
159 painter.fillRect(rects.fadeOutRect, grad);
160 }
161 painter.restore();
162 annotationRect->setRight(rects.fadeOutRect.right());
163}
164
165TextMark::AnnotationRects TextMark::annotationRects(const QRectF &boundingRect,
166 const QFontMetrics &fm,
167 const qreal fadeInOffset,
168 const qreal fadeOutOffset) const
169{
170 AnnotationRects rects;
171 rects.text = lineAnnotation();
172 if (rects.text.isEmpty())
173 return rects;
174 rects.fadeInRect = boundingRect;
175 rects.fadeInRect.setWidth(fadeInOffset);
176 rects.annotationRect = boundingRect;
177 rects.annotationRect.setLeft(rects.fadeInRect.right());
178 const bool drawIcon = !m_icon.isNull();
179 constexpr qreal margin = 1;
180 rects.iconRect = QRectF(rects.annotationRect.left(), boundingRect.top(),
181 0, boundingRect.height());
182 if (drawIcon)
183 rects.iconRect.setWidth(rects.iconRect.height() * m_widthFactor);
184 rects.textRect = QRectF(rects.iconRect.right() + margin, boundingRect.top(),
185 qreal(fm.horizontalAdvance(rects.text)), boundingRect.height());
186 rects.annotationRect.setRight(rects.textRect.right() + margin);
187 if (rects.annotationRect.right() > boundingRect.right()) {
188 rects.textRect.setRight(boundingRect.right() - margin);
189 rects.text = fm.elidedText(rects.text, Qt::ElideRight, int(rects.textRect.width()));
190 rects.annotationRect.setRight(boundingRect.right());
191 rects.fadeOutRect = QRectF(rects.annotationRect.topRight(),
192 rects.annotationRect.bottomRight());
193 } else {
194 rects.fadeOutRect = boundingRect;
195 rects.fadeOutRect.setLeft(rects.annotationRect.right());
196 rects.fadeOutRect.setWidth(fadeOutOffset);
197 }
198 return rects;
199}
200
201void TextMark::updateLineNumber(int lineNumber)
202{
203 m_lineNumber = lineNumber;
204}
205
206void TextMark::move(int line)
207{
208 if (line == m_lineNumber)
209 return;
210 const int previousLine = m_lineNumber;
211 m_lineNumber = line;
212 if (m_baseTextDocument)
213 m_baseTextDocument->moveMark(this, previousLine);
214}
215
216void TextMark::updateBlock(const QTextBlock &)
217{}
218
219void TextMark::removedFromEditor()
220{}
221
222void TextMark::updateMarker()
223{
224 if (m_baseTextDocument)
225 m_baseTextDocument->updateMark(this);
226}
227
228void TextMark::setPriority(TextMark::Priority prioriy)
229{
230 m_priority = prioriy;
231 updateMarker();
232}
233
234bool TextMark::isVisible() const
235{
236 return m_visible;
237}
238
239void TextMark::setVisible(bool visible)
240{
241 m_visible = visible;
242 updateMarker();
243}
244
245double TextMark::widthFactor() const
246{
247 return m_widthFactor;
248}
249
250void TextMark::setWidthFactor(double factor)
251{
252 m_widthFactor = factor;
253}
254
255bool TextMark::isClickable() const
256{
257 return false;
258}
259
260void TextMark::clicked()
261{}
262
263bool TextMark::isDraggable() const
264{
265 return false;
266}
267
268void TextMark::dragToLine(int lineNumber)
269{
270 Q_UNUSED(lineNumber)
271}
272
273void TextMark::addToToolTipLayout(QGridLayout *target) const
274{
275 auto contentLayout = new QVBoxLayout;
276 addToolTipContent(contentLayout);
277 if (contentLayout->count() <= 0)
278 return;
279
280 // Left column: text mark icon
281 const int row = target->rowCount();
282 if (!m_icon.isNull()) {
283 auto iconLabel = new QLabel;
284 iconLabel->setPixmap(m_icon.pixmap(16, 16));
285 target->addWidget(iconLabel, row, 0, Qt::AlignTop | Qt::AlignHCenter);
286 }
287
288 // Middle column: tooltip content
289 target->addLayout(contentLayout, row, 1);
290
291 // Right column: action icons/button
292 if (!m_actions.isEmpty()) {
293 auto actionsLayout = new QHBoxLayout;
294 QMargins margins = actionsLayout->contentsMargins();
295 margins.setLeft(margins.left() + 5);
296 actionsLayout->setContentsMargins(margins);
297 for (QAction *action : m_actions) {
298 QTC_ASSERT(!action->icon().isNull(), continue);
299 auto button = new QToolButton;
300 button->setIcon(action->icon());
301 QObject::connect(button, &QToolButton::clicked, action, &QAction::triggered);
302 QObject::connect(button, &QToolButton::clicked, []() {
303 Utils::ToolTip::hideImmediately();
304 });
305 actionsLayout->addWidget(button, 0, Qt::AlignTop | Qt::AlignRight);
306 }
307 target->addLayout(actionsLayout, row, 2);
308 }
309}
310
311bool TextMark::addToolTipContent(QLayout *target) const
312{
313 QString text = m_toolTip;
314 if (text.isEmpty()) {
315 text = m_defaultToolTip;
316 if (text.isEmpty())
317 return false;
318 }
319
320 auto textLabel = new QLabel;
321 textLabel->setOpenExternalLinks(true);
322 textLabel->setText(text);
323 // Differentiate between tool tips that where explicitly set and default tool tips.
324 textLabel->setEnabled(!m_toolTip.isEmpty());
325 target->addWidget(textLabel);
326
327 return true;
328}
329
330Theme::Color TextMark::color() const
331{
332 QTC_CHECK(m_hasColor);
333 return m_color;
334}
335
336void TextMark::setColor(const Theme::Color &color)
337{
338 m_hasColor = true;
339 m_color = color;
340}
341
342QVector<QAction *> TextMark::actions() const
343{
344 return m_actions;
345}
346
347void TextMark::setActions(const QVector<QAction *> &actions)
348{
349 m_actions = actions;
350}
351
352TextMarkRegistry::TextMarkRegistry(QObject *parent)
353 : QObject(parent)
354{
355 connect(EditorManager::instance(), &EditorManager::editorOpened,
356 this, &TextMarkRegistry::editorOpened);
357
358 connect(DocumentManager::instance(), &DocumentManager::allDocumentsRenamed,
359 this, &TextMarkRegistry::allDocumentsRenamed);
360 connect(DocumentManager::instance(), &DocumentManager::documentRenamed,
361 this, &TextMarkRegistry::documentRenamed);
362}
363
364void TextMarkRegistry::add(TextMark *mark)
365{
366 instance()->m_marks[mark->fileName()].insert(mark);
367 if (TextDocument *document = TextDocument::textDocumentForFileName(mark->fileName()))
368 document->addMark(mark);
369}
370
371bool TextMarkRegistry::remove(TextMark *mark)
372{
373 return instance()->m_marks[mark->fileName()].remove(mark);
374}
375
376TextMarkRegistry *TextMarkRegistry::instance()
377{
378 if (!m_instance)
379 m_instance = new TextMarkRegistry(TextEditorPlugin::instance());
380 return m_instance;
381}
382
383void TextMarkRegistry::editorOpened(IEditor *editor)
384{
385 auto document = qobject_cast<TextDocument *>(editor ? editor->document() : nullptr);
386 if (!document)
387 return;
388 if (!m_marks.contains(document->filePath()))
389 return;
390
391 foreach (TextMark *mark, m_marks.value(document->filePath()))
392 document->addMark(mark);
393}
394
395void TextMarkRegistry::documentRenamed(IDocument *document, const
396 QString &oldName, const QString &newName)
397{
398 auto baseTextDocument = qobject_cast<TextDocument *>(document);
399 if (!baseTextDocument)
400 return;
401 FilePath oldFileName = FilePath::fromString(oldName);
402 FilePath newFileName = FilePath::fromString(newName);
403 if (!m_marks.contains(oldFileName))
404 return;
405
406 QSet<TextMark *> toBeMoved;
407 foreach (TextMark *mark, baseTextDocument->marks())
408 toBeMoved.insert(mark);
409
410 m_marks[oldFileName].subtract(toBeMoved);
411 m_marks[newFileName].unite(toBeMoved);
412
413 foreach (TextMark *mark, toBeMoved)
414 mark->updateFileName(newFileName);
415}
416
417void TextMarkRegistry::allDocumentsRenamed(const QString &oldName, const QString &newName)
418{
419 FilePath oldFileName = FilePath::fromString(oldName);
420 FilePath newFileName = FilePath::fromString(newName);
421 if (!m_marks.contains(oldFileName))
422 return;
423
424 QSet<TextMark *> oldFileNameMarks = m_marks.value(oldFileName);
425
426 m_marks[newFileName].unite(oldFileNameMarks);
427 m_marks[oldFileName].clear();
428
429 foreach (TextMark *mark, oldFileNameMarks)
430 mark->updateFileName(newFileName);
431}
432
433QHash<AnnotationColors::SourceColors, AnnotationColors> AnnotationColors::m_colorCache;
434
435AnnotationColors &AnnotationColors::getAnnotationColors(const QColor &markColor,
436 const QColor &backgroundColor)
437{
438 auto highClipHsl = [](qreal value) {
439 return std::max(0.7, std::min(0.9, value));
440 };
441 auto lowClipHsl = [](qreal value) {
442 return std::max(0.1, std::min(0.3, value));
443 };
444 AnnotationColors &colors = m_colorCache[{markColor, backgroundColor}];
445 if (!colors.rectColor.isValid() || !colors.textColor.isValid()) {
446 const double backgroundLightness = backgroundColor.lightnessF();
447 const double foregroundLightness = backgroundLightness > 0.5
448 ? lowClipHsl(backgroundLightness - 0.5)
449 : highClipHsl(backgroundLightness + 0.5);
450
451 colors.rectColor = markColor;
452 colors.rectColor.setAlphaF(0.15);
453
454 colors.textColor.setHslF(markColor.hslHueF(),
455 markColor.hslSaturationF(),
456 foregroundLightness);
457 }
458 return colors;
459}
460
461} // namespace TextEditor
462
463#include "textmark.moc"
464