1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2008, 2009 Matthew Woehlke <mw_triad@users.sourceforge.net>
4 SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
5 SPDX-FileCopyrightText: 2002 John Firebaugh <jfirebaugh@kde.org>
6 SPDX-FileCopyrightText: 2001 Anders Lund <anders@alweb.dk>
7 SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
8 SPDX-FileCopyrightText: 2011 Svyatoslav Kuzmich <svatoslav1@gmail.com>
9 SPDX-FileCopyrightText: 2012 Kåre Särs <kare.sars@iki.fi> (Minimap)
10 SPDX-FileCopyrightText: 2017-2018 Friedrich W. H. Kossebau <kossebau@kde.org>
11
12 SPDX-License-Identifier: LGPL-2.0-only
13*/
14
15#include "kateviewhelpers.h"
16
17#include "kateabstractinputmode.h"
18#include "kateannotationitemdelegate.h"
19#include "katecmd.h"
20#include "katecommandrangeexpressionparser.h"
21#include "kateconfig.h"
22#include "katedocument.h"
23#include "kateglobal.h"
24#include "katelayoutcache.h"
25#include "katepartdebug.h"
26#include "katerenderer.h"
27#include "katesyntaxmanager.h"
28#include "katetextlayout.h"
29#include "katetextpreview.h"
30#include "kateview.h"
31#include "kateviewinternal.h"
32#include <katebuffer.h>
33#include <ktexteditor/annotationinterface.h>
34#include <ktexteditor/attribute.h>
35#include <ktexteditor/command.h>
36#include <ktexteditor/movingrange.h>
37
38#include <KActionCollection>
39#include <KCharsets>
40#include <KColorUtils>
41#include <KConfigGroup>
42#include <KHelpClient>
43#include <KLocalizedString>
44
45#include <QAction>
46#include <QActionGroup>
47#include <QBoxLayout>
48#include <QCursor>
49#include <QGuiApplication>
50#include <QKeyEvent>
51#include <QLinearGradient>
52#include <QMenu>
53#include <QPainter>
54#include <QPainterPath>
55#include <QPalette>
56#include <QPen>
57#include <QRegularExpression>
58#include <QStackedWidget>
59#include <QStyle>
60#include <QStyleOption>
61#include <QToolButton>
62#include <QToolTip>
63#include <QVariant>
64#include <QWhatsThis>
65#include <QtAlgorithms>
66
67#include <math.h>
68
69// BEGIN KateMessageLayout
70KateMessageLayout::KateMessageLayout(QWidget *parent)
71 : QLayout(parent)
72{
73 qCDebug(LOG_KTE);
74}
75
76KateMessageLayout::~KateMessageLayout()
77{
78 while (QLayoutItem *item = takeAt(index: 0)) {
79 delete item;
80 }
81}
82
83void KateMessageLayout::addItem(QLayoutItem *item)
84{
85 Q_ASSERT(false);
86 add(item, pos: KTextEditor::Message::CenterInView);
87}
88
89void KateMessageLayout::addWidget(QWidget *widget, KTextEditor::Message::MessagePosition pos)
90{
91 add(item: new QWidgetItem(widget), pos);
92}
93
94int KateMessageLayout::count() const
95{
96 return m_items.size();
97}
98
99QLayoutItem *KateMessageLayout::itemAt(int index) const
100{
101 return m_items.value(i: index).item;
102}
103
104void KateMessageLayout::setGeometry(const QRect &rect)
105{
106 QLayout::setGeometry(rect);
107 const int s = spacing();
108 const QRect adjustedRect = rect.adjusted(xp1: s, yp1: s, xp2: -s, yp2: -s);
109
110 for (const auto &wrapper : std::as_const(t&: m_items)) {
111 QLayoutItem *item = wrapper.item;
112 auto position = wrapper.position;
113
114 if (position == KTextEditor::Message::TopInView) {
115 const QRect r(adjustedRect.width() - item->sizeHint().width(), s, item->sizeHint().width(), item->sizeHint().height());
116 item->setGeometry(r);
117 } else if (position == KTextEditor::Message::BottomInView) {
118 const QRect r(adjustedRect.width() - item->sizeHint().width(),
119 adjustedRect.height() - item->sizeHint().height(),
120 item->sizeHint().width(),
121 item->sizeHint().height());
122 item->setGeometry(r);
123 } else if (position == KTextEditor::Message::CenterInView) {
124 QRect r(0, 0, item->sizeHint().width(), item->sizeHint().height());
125 r.moveCenter(p: adjustedRect.center());
126 item->setGeometry(r);
127 } else {
128 Q_ASSERT_X(false, "setGeometry", "Only TopInView, CenterInView, and BottomInView are supported.");
129 }
130 }
131}
132
133QSize KateMessageLayout::sizeHint() const
134{
135 return QSize();
136}
137
138QLayoutItem *KateMessageLayout::takeAt(int index)
139{
140 if (index >= 0 && index < m_items.size()) {
141 return m_items.takeAt(i: index).item;
142 }
143 return nullptr;
144}
145
146void KateMessageLayout::add(QLayoutItem *item, KTextEditor::Message::MessagePosition pos)
147{
148 m_items.push_back(t: {item, pos});
149}
150// END KateMessageLayout
151
152// BEGIN KateScrollBar
153static const int s_lineWidth = 100;
154static const int s_pixelMargin = 8;
155static const int s_linePixelIncLimit = 6;
156
157KateScrollBar::KateScrollBar(Qt::Orientation orientation, KateViewInternal *parent)
158 : QScrollBar(orientation, parent->m_view)
159 , m_middleMouseDown(false)
160 , m_leftMouseDown(false)
161 , m_view(parent->m_view)
162 , m_doc(parent->doc())
163 , m_viewInternal(parent)
164 , m_textPreview(nullptr)
165 , m_showMarks(false)
166 , m_showMiniMap(false)
167 , m_miniMapAll(true)
168 , m_needsUpdateOnShow(false)
169 , m_miniMapWidth(40)
170 , m_grooveHeight(height())
171 , m_tooltipLineNoInfo(this)
172{
173 connect(sender: this, signal: &KateScrollBar::valueChanged, context: this, slot: &KateScrollBar::sliderMaybeMoved);
174 connect(sender: m_doc, signal: &KTextEditor::DocumentPrivate::marksChanged, context: this, slot: &KateScrollBar::marksChanged);
175
176 m_updateTimer.setInterval(300);
177 m_updateTimer.setSingleShot(true);
178
179 // track mouse for text preview widget
180 setMouseTracking(orientation == Qt::Vertical);
181
182 // setup text preview timer
183 m_delayTextPreviewTimer.setSingleShot(true);
184 m_delayTextPreviewTimer.setInterval(250);
185 connect(sender: &m_delayTextPreviewTimer, signal: &QTimer::timeout, context: this, slot: &KateScrollBar::showTextPreview);
186
187 m_tooltipLineNoInfo.setTextFormat(Qt::TextFormat::RichText);
188 m_tooltipLineNoInfo.setBackgroundRole(QPalette::Window);
189 m_tooltipLineNoInfo.setContentsMargins({2, 2, 2, 2});
190 m_tooltipLineNoInfo.setAutoFillBackground(true);
191 m_tooltipLineNoInfo.setVisible(false);
192}
193
194void KateScrollBar::showEvent(QShowEvent *event)
195{
196 QScrollBar::showEvent(event);
197
198 if (m_needsUpdateOnShow) {
199 m_needsUpdateOnShow = false;
200 updatePixmap();
201 }
202}
203
204KateScrollBar::~KateScrollBar()
205{
206 delete m_textPreview;
207}
208
209void KateScrollBar::setShowMiniMap(bool b)
210{
211 if (b && !m_showMiniMap) {
212 auto timerSlot = qOverload<>(&QTimer::start);
213 connect(sender: m_view, signal: &KTextEditor::ViewPrivate::selectionChanged, context: &m_updateTimer, slot&: timerSlot, type: Qt::UniqueConnection);
214 connect(sender: m_doc, signal: &KTextEditor::DocumentPrivate::textChanged, context: &m_updateTimer, slot&: timerSlot, type: Qt::UniqueConnection);
215 connect(sender: m_view, signal: &KTextEditor::ViewPrivate::delayedUpdateOfView, context: &m_updateTimer, slot&: timerSlot, type: Qt::UniqueConnection);
216 connect(sender: &m_updateTimer, signal: &QTimer::timeout, context: this, slot: &KateScrollBar::updatePixmap, type: Qt::UniqueConnection);
217 connect(sender: &(m_view->textFolding()), signal: &Kate::TextFolding::foldingRangesChanged, context: &m_updateTimer, slot&: timerSlot, type: Qt::UniqueConnection);
218 } else if (!b) {
219 disconnect(receiver: &m_updateTimer);
220 }
221
222 m_showMiniMap = b;
223
224 updateGeometry();
225 update();
226}
227
228QSize KateScrollBar::sizeHint() const
229{
230 if (m_showMiniMap) {
231 return QSize(m_miniMapWidth, QScrollBar::sizeHint().height());
232 }
233 return QScrollBar::sizeHint();
234}
235
236int KateScrollBar::minimapYToStdY(int y)
237{
238 // Check if the minimap fills the whole scrollbar
239 if (m_stdGroveRect.height() == m_mapGroveRect.height()) {
240 return y;
241 }
242
243 // check if y is on the step up/down
244 if ((y < m_stdGroveRect.top()) || (y > m_stdGroveRect.bottom())) {
245 return y;
246 }
247
248 if (y < m_mapGroveRect.top()) {
249 return m_stdGroveRect.top() + 1;
250 }
251
252 if (y > m_mapGroveRect.bottom()) {
253 return m_stdGroveRect.bottom() - 1;
254 }
255
256 // check for div/0
257 if (m_mapGroveRect.height() == 0) {
258 return y;
259 }
260
261 int newY = (y - m_mapGroveRect.top()) * m_stdGroveRect.height() / m_mapGroveRect.height();
262 newY += m_stdGroveRect.top();
263 return newY;
264}
265
266void KateScrollBar::mousePressEvent(QMouseEvent *e)
267{
268 // delete text preview
269 hideTextPreview();
270
271 if (e->button() == Qt::MiddleButton) {
272 m_middleMouseDown = true;
273 } else if (e->button() == Qt::LeftButton) {
274 m_leftMouseDown = true;
275 }
276
277 if (m_showMiniMap) {
278 if (!m_sliderRect.contains(p: e->pos()) && m_leftMouseDown && e->pos().y() > m_mapGroveRect.top() && e->pos().y() < m_mapGroveRect.bottom()) {
279 // if we show the minimap left-click jumps directly to the selected position
280 int newVal = (e->pos().y() - m_mapGroveRect.top()) / (double)m_mapGroveRect.height() * (double)(maximum() + pageStep()) - pageStep() / 2;
281 newVal = qBound(min: 0, val: newVal, max: maximum());
282 setSliderPosition(newVal);
283 }
284 const QPoint pos(6, minimapYToStdY(y: e->pos().y()));
285 QMouseEvent eMod(QEvent::MouseButtonPress, pos, mapToGlobal(pos), e->button(), e->buttons(), e->modifiers());
286 QScrollBar::mousePressEvent(&eMod);
287 } else {
288 QScrollBar::mousePressEvent(e);
289 }
290
291 auto toolTipPos = e->globalPosition().toPoint() - QPoint(e->pos().x(), 0);
292 toolTipPos.ry() += m_sliderRect.height();
293 m_toolTipPos = toolTipPos;
294 const int fromLine = m_viewInternal->toRealCursor(virtualCursor: m_viewInternal->startPos()).line() + 1;
295 const int lastLine = m_viewInternal->toRealCursor(virtualCursor: m_viewInternal->endPos()).line() + 1;
296 const QString text = i18nc("from line - to line", "<center>%1<br/>&#x2014;<br/>%2</center>", fromLine, lastLine);
297 m_tooltipLineNoInfo.setText(text);
298 m_tooltipLineNoInfo.setVisible(m_leftMouseDown);
299 m_tooltipLineNoInfo.adjustSize();
300 m_tooltipLineNoInfo.move(mapFromGlobal(toolTipPos));
301
302 redrawMarks();
303}
304
305void KateScrollBar::mouseReleaseEvent(QMouseEvent *e)
306{
307 if (e->button() == Qt::MiddleButton) {
308 m_middleMouseDown = false;
309 } else if (e->button() == Qt::LeftButton) {
310 m_leftMouseDown = false;
311 }
312
313 redrawMarks();
314
315 m_tooltipLineNoInfo.setVisible(false);
316
317 if (m_showMiniMap) {
318 const QPoint pos(e->pos().x(), minimapYToStdY(y: e->pos().y()));
319 QMouseEvent eMod(QEvent::MouseButtonRelease, pos, mapToGlobal(pos), e->button(), e->buttons(), e->modifiers());
320 QScrollBar::mouseReleaseEvent(&eMod);
321 } else {
322 QScrollBar::mouseReleaseEvent(e);
323 }
324}
325
326void KateScrollBar::mouseMoveEvent(QMouseEvent *e)
327{
328 if (m_showMiniMap) {
329 const QPoint pos(e->pos().x(), minimapYToStdY(y: e->pos().y()));
330 QMouseEvent eMod(QEvent::MouseMove, pos, mapToGlobal(pos), e->button(), e->buttons(), e->modifiers());
331 QScrollBar::mouseMoveEvent(&eMod);
332 } else {
333 QScrollBar::mouseMoveEvent(e);
334 }
335
336 if (e->buttons() & (Qt::LeftButton | Qt::MiddleButton)) {
337 redrawMarks();
338
339 // current line tool tip
340 auto toolTipPos = e->globalPosition().toPoint() - QPoint(e->pos().x(), 0);
341 toolTipPos.ry() += m_sliderRect.height();
342 m_toolTipPos = toolTipPos;
343 const int fromLine = m_viewInternal->toRealCursor(virtualCursor: m_viewInternal->startPos()).line() + 1;
344 const int lastLine = m_viewInternal->toRealCursor(virtualCursor: m_viewInternal->endPos()).line() + 1;
345 const QString text = i18nc("from line - to line", "<center>%1<br/>&#x2014;<br/>%2</center>", fromLine, lastLine);
346 m_tooltipLineNoInfo.setText(text);
347 m_tooltipLineNoInfo.setVisible(true);
348 m_tooltipLineNoInfo.adjustSize();
349 m_tooltipLineNoInfo.move(mapFromGlobal(toolTipPos));
350 }
351
352 showTextPreviewDelayed();
353}
354
355void KateScrollBar::leaveEvent(QEvent *event)
356{
357 hideTextPreview();
358
359 QAbstractSlider::leaveEvent(event);
360}
361
362bool KateScrollBar::eventFilter(QObject *object, QEvent *event)
363{
364 Q_UNUSED(object)
365
366 if (m_textPreview && event->type() == QEvent::WindowDeactivate) {
367 // We need hide the scrollbar TextPreview widget
368 hideTextPreview();
369 }
370
371 return false;
372}
373
374void KateScrollBar::paintEvent(QPaintEvent *e)
375{
376 if (m_doc->marks().size() != m_lines.size()) {
377 recomputeMarksPositions();
378 }
379 if (m_showMiniMap) {
380 miniMapPaintEvent(e);
381 } else {
382 normalPaintEvent(e);
383 }
384}
385
386void KateScrollBar::showTextPreviewDelayed()
387{
388 if (!m_textPreview) {
389 if (!m_delayTextPreviewTimer.isActive()) {
390 m_delayTextPreviewTimer.start();
391 }
392 } else {
393 showTextPreview();
394 }
395}
396
397void KateScrollBar::showTextPreview()
398{
399 if (orientation() != Qt::Vertical || isSliderDown() || (minimum() == maximum()) || !m_view->config()->scrollBarPreview()) {
400 return;
401 }
402
403 // only show when main window is active (#392396)
404 if (window() && !window()->isActiveWindow()) {
405 return;
406 }
407
408 QRect grooveRect;
409 if (m_showMiniMap) {
410 // If mini-map is shown, the height of the map might not be the whole height
411 grooveRect = m_mapGroveRect;
412 } else {
413 QStyleOptionSlider opt;
414 opt.initFrom(w: this);
415 opt.subControls = QStyle::SC_None;
416 opt.activeSubControls = QStyle::SC_None;
417 opt.orientation = orientation();
418 opt.minimum = minimum();
419 opt.maximum = maximum();
420 opt.sliderPosition = sliderPosition();
421 opt.sliderValue = value();
422 opt.singleStep = singleStep();
423 opt.pageStep = pageStep();
424
425 grooveRect = style()->subControlRect(cc: QStyle::CC_ScrollBar, opt: &opt, sc: QStyle::SC_ScrollBarGroove, widget: this);
426 }
427
428 if (m_view->config()->scrollPastEnd()) {
429 // Adjust the grove size to accommodate the added pageStep at the bottom
430 int adjust = pageStep() * grooveRect.height() / (maximum() + pageStep() - minimum());
431 grooveRect.adjust(dx1: 0, dy1: 0, dx2: 0, dy2: -adjust);
432 }
433
434 const QPoint cursorPos = mapFromGlobal(QCursor::pos());
435 if (grooveRect.contains(p: cursorPos)) {
436 if (!m_textPreview) {
437 m_textPreview = new KateTextPreview(m_view, this);
438 m_textPreview->setAttribute(Qt::WA_ShowWithoutActivating);
439 m_textPreview->setFrameStyle(QFrame::StyledPanel);
440
441 // event filter to catch application WindowDeactivate event, to hide the preview window
442 qApp->installEventFilter(filterObj: this);
443 }
444
445 const qreal posInPercent = static_cast<double>(cursorPos.y() - grooveRect.top()) / grooveRect.height();
446 const qreal startLine = posInPercent * m_view->textFolding().visibleLines();
447
448 m_textPreview->resize(w: m_view->width() / 2, h: m_view->height() / 5);
449 const int xGlobal = mapToGlobal(QPoint(0, 0)).x();
450 const int yGlobal = qMin(a: mapToGlobal(QPoint(0, height())).y() - m_textPreview->height(),
451 b: qMax(a: mapToGlobal(QPoint(0, 0)).y(), b: mapToGlobal(cursorPos).y() - m_textPreview->height() / 2));
452 m_textPreview->move(ax: xGlobal - m_textPreview->width(), ay: yGlobal);
453 m_textPreview->setLine(startLine);
454 m_textPreview->setCenterView(true);
455 m_textPreview->setScaleFactor(0.75);
456 m_textPreview->raise();
457 m_textPreview->show();
458 } else {
459 hideTextPreview();
460 }
461}
462
463void KateScrollBar::hideTextPreview()
464{
465 if (m_delayTextPreviewTimer.isActive()) {
466 m_delayTextPreviewTimer.stop();
467 }
468
469 qApp->removeEventFilter(obj: this);
470 delete m_textPreview;
471}
472
473// This function is optimized for bing called in sequence.
474void KateScrollBar::getCharColorRanges(const QList<Kate::TextLine::Attribute> &attributes,
475 const QList<Kate::TextRange *> &decorations,
476 const QString &text,
477 QList<KateScrollBar::ColumnRangeWithColor> &ranges,
478 QVarLengthArray<std::pair<QRgb, QPen>, 20> &penCache)
479{
480 ranges.clear();
481
482 auto getPen = [&](const QBrush &color) -> int {
483 uint rgb = color.color().rgb();
484 auto it = std::find_if(first: penCache.begin(), last: penCache.end(), pred: [rgb](const std::pair<QRgb, QPen> &rgbToPen) {
485 return rgb == rgbToPen.first;
486 });
487 if (it != penCache.end()) {
488 return it - penCache.begin();
489 }
490 penCache.push_back(t: {rgb, QPen(color, 1)});
491 return (int)penCache.size() - 1;
492 };
493
494 constexpr QChar space = QLatin1Char(' ');
495 constexpr QChar tab = QLatin1Char('\t');
496
497 for (int i = 0; i < text.size() && i < s_lineWidth; ++i) {
498 if (text[i] == space || text[i] == tab) {
499 continue;
500 }
501
502 bool styleFound = false;
503 for (auto range : decorations) {
504 if (range->containsColumn(column: i)) {
505 QBrush color = range->attribute()->foreground();
506 styleFound = true;
507 int startCol = range->start().column();
508 int endCol = range->end().column();
509 ranges << ColumnRangeWithColor{.penIndex = getPen(color), .startColumn = startCol, .endColumn = endCol};
510 i = endCol;
511 break;
512 }
513 }
514
515 if (styleFound) {
516 continue;
517 }
518
519 // If there's no decoration set for the current character (this will mostly be the case for
520 // plain Kate), query the styles, that is, the default kate syntax highlighting.
521 // go to the block containing x
522 qsizetype attributeIndex = 0;
523 while ((attributeIndex < attributes.size()) && ((attributes[attributeIndex].offset + attributes[attributeIndex].length) < i)) {
524 ++attributeIndex;
525 }
526 if (attributeIndex < attributes.size()) {
527 const auto attr = attributes[attributeIndex];
528 if ((i < attr.offset + attr.length)) {
529 QBrush color = m_view->renderer()->attribute(pos: attr.attributeValue)->foreground();
530 int startCol = attr.offset;
531 int endCol = attr.offset + attr.length;
532 ranges << ColumnRangeWithColor{.penIndex = getPen(color), .startColumn = startCol, .endColumn = endCol};
533 i = endCol;
534 }
535 }
536 }
537}
538
539void KateScrollBar::updatePixmap()
540{
541 // QElapsedTimer time;
542 // time.start();
543
544 if (!m_showMiniMap) {
545 // make sure no time is wasted if the option is disabled
546 return;
547 }
548
549 if (!isVisible()) {
550 // don't update now if the document is not visible; do it when
551 // the document is shown again instead
552 m_needsUpdateOnShow = true;
553 return;
554 }
555
556 // For performance reason, only every n-th line will be drawn if the widget is
557 // sufficiently small compared to the amount of lines in the document.
558 int docLineCount = m_view->textFolding().visibleLines();
559 int pixmapLineCount = docLineCount;
560 if (m_view->config()->scrollPastEnd()) {
561 pixmapLineCount += pageStep();
562 }
563 int pixmapLinesUnscaled = pixmapLineCount;
564 if (m_grooveHeight < 5) {
565 m_grooveHeight = 5;
566 }
567 int charIncrement = 1;
568 int lineIncrement = 1;
569 if ((m_grooveHeight > 10) && (pixmapLineCount >= m_grooveHeight * 2)) {
570 charIncrement = pixmapLineCount / m_grooveHeight;
571 while (charIncrement > s_linePixelIncLimit) {
572 lineIncrement++;
573 pixmapLineCount = pixmapLinesUnscaled / lineIncrement;
574 charIncrement = pixmapLineCount / m_grooveHeight;
575 }
576 pixmapLineCount /= charIncrement;
577 }
578
579 int pixmapLineWidth = s_pixelMargin + s_lineWidth / charIncrement;
580
581 // qCDebug(LOG_KTE) << "l" << lineIncrement << "c" << charIncrement << "d";
582 // qCDebug(LOG_KTE) << "pixmap" << pixmapLineCount << pixmapLineWidth << "docLines" << m_view->textFolding().visibleLines() << "height" << m_grooveHeight;
583
584 const QBrush backgroundColor = m_view->defaultStyleAttribute(defaultStyle: KSyntaxHighlighting::Theme::TextStyle::Normal)->background();
585 const QBrush defaultTextColor = m_view->defaultStyleAttribute(defaultStyle: KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground();
586 const QBrush selectionBgColor = m_view->rendererConfig()->selectionColor();
587
588 QColor modifiedLineColor = m_view->rendererConfig()->modifiedLineColor();
589 QColor savedLineColor = m_view->rendererConfig()->savedLineColor();
590 // move the modified line color away from the background color
591 modifiedLineColor.setHsv(h: modifiedLineColor.hue(), s: 255, v: 255 - backgroundColor.color().value() / 3);
592 savedLineColor.setHsv(h: savedLineColor.hue(), s: 100, v: 255 - backgroundColor.color().value() / 3);
593
594 const QBrush modifiedLineBrush = modifiedLineColor;
595 const QBrush savedLineBrush = savedLineColor;
596
597 // increase dimensions by ratio
598 m_pixmap = QPixmap(pixmapLineWidth * m_view->devicePixelRatioF(), pixmapLineCount * m_view->devicePixelRatioF());
599 m_pixmap.fill(fillColor: QColor("transparent"));
600
601 // The text currently selected in the document, to be drawn later.
602 const KTextEditor::Range selection = m_view->selectionRange();
603 const bool hasSelection = !selection.isEmpty();
604 // reusable buffer for color->range
605 QList<KateScrollBar::ColumnRangeWithColor> colorRangesForLine;
606 const QPen defaultTextPen = QPen(defaultTextColor, 1);
607 // resusable buffer for line ranges;
608 QList<Kate::TextRange *> decorations;
609
610 QPainter painter;
611 if (painter.begin(&m_pixmap)) {
612 // init pen once, afterwards, only change it if color changes to avoid a lot of allocation for setPen
613 painter.setPen(QPen(selectionBgColor, 1));
614
615 // Do not force updates of the highlighting if the document is very large
616 const bool simpleMode = m_doc->lines() > 7500;
617
618 int pixelY = 0;
619 int drawnLines = 0;
620
621 // pen cache to avoid a lot of allocations from pen creation
622 QVarLengthArray<std::pair<QRgb, QPen>, 20> penCache;
623
624 // Iterate over all visible lines, drawing them.
625 for (int virtualLine = 0; virtualLine < docLineCount; virtualLine += lineIncrement) {
626 int realLineNumber = m_view->textFolding().visibleLineToLine(visibleLine: virtualLine);
627 const Kate::TextLine kateline = m_doc->plainKateTextLine(i: realLineNumber);
628 const QString lineText = kateline.text();
629
630 if (!simpleMode) {
631 m_doc->buffer().ensureHighlighted(line: realLineNumber);
632 }
633
634 // get normal highlighting stuff
635 const auto &attributes = kateline.attributesList();
636
637 // get moving ranges with attribs (semantic highlighting and co.)
638 m_view->doc()->buffer().rangesForLine(line: realLineNumber, view: m_view, rangesWithAttributeOnly: true, outRanges&: decorations);
639
640 // Draw selection if it is on an empty line
641
642 int pixelX = s_pixelMargin; // use this to control the offset of the text from the left
643
644 if (hasSelection) {
645 if (selection.contains(cursor: KTextEditor::Cursor(realLineNumber, 0)) && lineText.size() == 0) {
646 if (selectionBgColor != painter.pen().brush()) {
647 painter.setPen(QPen(selectionBgColor, 1));
648 }
649 painter.drawLine(x1: s_pixelMargin, y1: pixelY, x2: s_pixelMargin + s_lineWidth - 1, y2: pixelY);
650 }
651 // Iterate over the line to draw the background
652 int selStartX = -1;
653 int selEndX = -1;
654 for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) {
655 if (pixelX >= s_lineWidth + s_pixelMargin) {
656 break;
657 }
658 // Query the selection and draw it behind the character
659 if (selection.contains(cursor: KTextEditor::Cursor(realLineNumber, x))) {
660 if (selStartX == -1) {
661 selStartX = pixelX;
662 }
663 selEndX = pixelX;
664 if (lineText.size() - 1 == x) {
665 selEndX = s_lineWidth + s_pixelMargin - 1;
666 }
667 }
668
669 if (lineText[x] == QLatin1Char('\t')) {
670 pixelX += qMax(a: 4 / charIncrement, b: 1); // FIXME: tab width...
671 } else {
672 pixelX++;
673 }
674 }
675
676 if (selStartX != -1) {
677 if (selectionBgColor != painter.pen().brush()) {
678 painter.setPen(QPen(selectionBgColor, 1));
679 }
680 painter.drawLine(x1: selStartX, y1: pixelY, x2: selEndX, y2: pixelY);
681 }
682 }
683
684 // Iterate over all the characters in the current line
685 getCharColorRanges(attributes, decorations, text: lineText, ranges&: colorRangesForLine, penCache);
686 pixelX = s_pixelMargin;
687 for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) {
688 if (pixelX >= s_lineWidth + s_pixelMargin) {
689 break;
690 }
691
692 // draw the pixels
693 if (lineText[x] == QLatin1Char(' ')) {
694 pixelX++;
695 } else if (lineText[x] == QLatin1Char('\t')) {
696 pixelX += qMax(a: 4 / charIncrement, b: 1); // FIXME: tab width...
697 } else {
698 const QPen *pen = nullptr;
699 int rangeEnd = x + 1;
700 for (const auto &cr : colorRangesForLine) {
701 if (cr.startColumn <= x && x <= cr.endColumn) {
702 rangeEnd = cr.endColumn;
703 if (cr.penIndex != -1) {
704 pen = &penCache[cr.penIndex].second;
705 }
706 }
707 }
708
709 if (!pen) {
710 pen = &defaultTextPen;
711 }
712 // get the column range and color in which this 'x' lies
713 painter.setPen(*pen);
714
715 // Actually draw the pixels with the color queried from the renderer.
716 QVarLengthArray<QPoint, 100> points;
717 for (; x < rangeEnd; x += charIncrement) {
718 if (pixelX >= s_lineWidth + s_pixelMargin) {
719 break;
720 }
721 points.append(t: {pixelX++, pixelY});
722 }
723 painter.drawPoints(points: points.data(), pointCount: points.size());
724 }
725 }
726 drawnLines++;
727 if (((drawnLines) % charIncrement) == 0) {
728 pixelY++;
729 }
730 }
731 // qCDebug(LOG_KTE) << drawnLines;
732 // Draw line modification marker map.
733 // Disable this if the document is really huge,
734 // since it requires querying every line.
735 if (m_doc->lines() < 50000) {
736 for (int lineno = 0; lineno < docLineCount; lineno++) {
737 int realLineNo = m_view->textFolding().visibleLineToLine(visibleLine: lineno);
738 const auto line = m_doc->plainKateTextLine(i: realLineNo);
739 const QBrush &col = line.markedAsModified() ? modifiedLineBrush : savedLineBrush;
740 if (line.markedAsModified() || line.markedAsSavedOnDisk()) {
741 int pos = (lineno * pixmapLineCount) / pixmapLinesUnscaled;
742 painter.fillRect(x: 2, y: pos, w: 3, h: 1, b: col);
743 }
744 }
745 }
746
747 // end painting
748 painter.end();
749 }
750
751 // set right ratio
752 m_pixmap.setDevicePixelRatio(m_view->devicePixelRatioF());
753
754 // qCDebug(LOG_KTE) << time.elapsed();
755 // Redraw the scrollbar widget with the updated pixmap.
756 update();
757}
758
759void KateScrollBar::miniMapPaintEvent(QPaintEvent *e)
760{
761 QScrollBar::paintEvent(e);
762
763 QPainter painter(this);
764
765 QStyleOptionSlider opt;
766 opt.initFrom(w: this);
767 opt.subControls = QStyle::SC_None;
768 opt.activeSubControls = QStyle::SC_None;
769 opt.orientation = orientation();
770 opt.minimum = minimum();
771 opt.maximum = maximum();
772 opt.sliderPosition = sliderPosition();
773 opt.sliderValue = value();
774 opt.singleStep = singleStep();
775 opt.pageStep = pageStep();
776
777 QRect grooveRect = style()->subControlRect(cc: QStyle::CC_ScrollBar, opt: &opt, sc: QStyle::SC_ScrollBarGroove, widget: this);
778 m_stdGroveRect = grooveRect;
779 if (style()->subControlRect(cc: QStyle::CC_ScrollBar, opt: &opt, sc: QStyle::SC_ScrollBarSubLine, widget: this).height() == 0) {
780 int alignMargin = style()->pixelMetric(metric: QStyle::PM_FocusFrameVMargin, option: &opt, widget: this);
781 grooveRect.moveTop(pos: alignMargin);
782 grooveRect.setHeight(grooveRect.height() - alignMargin);
783 }
784 if (style()->subControlRect(cc: QStyle::CC_ScrollBar, opt: &opt, sc: QStyle::SC_ScrollBarAddLine, widget: this).height() == 0) {
785 int alignMargin = style()->pixelMetric(metric: QStyle::PM_FocusFrameVMargin, option: &opt, widget: this);
786 grooveRect.setHeight(grooveRect.height() - alignMargin);
787 }
788 m_grooveHeight = grooveRect.height();
789
790 const int docXMargin = 1;
791 // style()->drawControl(QStyle::CE_ScrollBarAddLine, &opt, &painter, this);
792 // style()->drawControl(QStyle::CE_ScrollBarSubLine, &opt, &painter, this);
793
794 // calculate the document size and position
795 const int docHeight = qMin(a: grooveRect.height(), b: int(m_pixmap.height() / m_pixmap.devicePixelRatio() * 2)) - 2 * docXMargin;
796 const int yoffset = 1; // top-aligned in stead of center-aligned (grooveRect.height() - docHeight) / 2;
797 const QRect docRect(QPoint(grooveRect.left() + docXMargin, yoffset + grooveRect.top()), QSize(grooveRect.width() - docXMargin, docHeight));
798 m_mapGroveRect = docRect;
799
800 // calculate the visible area
801 int max = qMax(a: maximum() + 1, b: 1);
802 int visibleStart = value() * docHeight / (max + pageStep()) + docRect.top() + 0.5;
803 int visibleEnd = (value() + pageStep()) * docHeight / (max + pageStep()) + docRect.top();
804 QRect visibleRect = docRect;
805 visibleRect.moveTop(pos: visibleStart);
806 visibleRect.setHeight(visibleEnd - visibleStart);
807
808 // calculate colors
809 const QColor backgroundColor = m_view->defaultStyleAttribute(defaultStyle: KSyntaxHighlighting::Theme::TextStyle::Normal)->background().color();
810 const QColor foregroundColor = m_view->defaultStyleAttribute(defaultStyle: KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color();
811 const QColor highlightColor = palette().highlight().color();
812
813 const int backgroundLightness = backgroundColor.lightness();
814 const int foregroundLightness = foregroundColor.lightness();
815 const int lighnessDiff = (foregroundLightness - backgroundLightness);
816
817 // get a color suited for the color theme
818 QColor darkShieldColor = palette().color(cr: QPalette::Mid);
819 int hue;
820 int sat;
821 int light;
822 darkShieldColor.getHsl(h: &hue, s: &sat, l: &light);
823 // apply suitable lightness
824 darkShieldColor.setHsl(h: hue, s: sat, l: backgroundLightness + lighnessDiff * 0.35);
825 // gradient for nicer results
826 QLinearGradient gradient(0, 0, width(), 0);
827 gradient.setColorAt(pos: 0, color: darkShieldColor);
828 gradient.setColorAt(pos: 0.3, color: darkShieldColor.lighter(f: 115));
829 gradient.setColorAt(pos: 1, color: darkShieldColor);
830
831 QColor lightShieldColor;
832 lightShieldColor.setHsl(h: hue, s: sat, l: backgroundLightness + lighnessDiff * 0.15);
833
834 QColor outlineColor;
835 outlineColor.setHsl(h: hue, s: sat, l: backgroundLightness + lighnessDiff * 0.5);
836
837 // draw the grove background in case the document is small
838 painter.setPen(Qt::NoPen);
839 painter.setBrush(backgroundColor);
840 painter.drawRect(r: grooveRect);
841
842 // adjust the rectangles
843 QRect sliderRect = style()->subControlRect(cc: QStyle::CC_ScrollBar, opt: &opt, sc: QStyle::SC_ScrollBarSlider, widget: this);
844 sliderRect.setX(docXMargin);
845 sliderRect.setWidth(width() - docXMargin * 2);
846
847 if ((docHeight + 2 * docXMargin >= grooveRect.height()) && (sliderRect.height() > visibleRect.height() + 2)) {
848 visibleRect.adjust(dx1: 2, dy1: 0, dx2: -3, dy2: 0);
849 } else {
850 visibleRect.adjust(dx1: 1, dy1: 0, dx2: -1, dy2: 2);
851 sliderRect.setTop(visibleRect.top() - 1);
852 sliderRect.setBottom(visibleRect.bottom() + 1);
853 }
854
855 // Smooth transform only when squeezing
856 if (grooveRect.height() < m_pixmap.height() / m_pixmap.devicePixelRatio()) {
857 painter.setRenderHint(hint: QPainter::SmoothPixmapTransform);
858 }
859
860 // draw the modified lines margin
861 QRect pixmapMarginRect(QPoint(0, 0), QSize(s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio()));
862 QRect docPixmapMarginRect(QPoint(0, docRect.top()), QSize(s_pixelMargin, docRect.height()));
863 painter.drawPixmap(targetRect: docPixmapMarginRect, pixmap: m_pixmap, sourceRect: pixmapMarginRect);
864
865 // calculate the stretch and draw the stretched lines (scrollbar marks)
866 QRect pixmapRect(QPoint(s_pixelMargin, 0),
867 QSize(m_pixmap.width() / m_pixmap.devicePixelRatio() - s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio()));
868 QRect docPixmapRect(QPoint(s_pixelMargin, docRect.top()), QSize(docRect.width() - s_pixelMargin, docRect.height()));
869 painter.drawPixmap(targetRect: docPixmapRect, pixmap: m_pixmap, sourceRect: pixmapRect);
870
871 // delimit the end of the document
872 const int y = docPixmapRect.height() + grooveRect.y();
873 if (y + 2 < grooveRect.y() + grooveRect.height()) {
874 QColor fg(foregroundColor);
875 fg.setAlpha(30);
876 painter.setBrush(Qt::NoBrush);
877 painter.setPen(QPen(fg, 1));
878 painter.drawLine(x1: grooveRect.x() + 1, y1: y + 2, x2: width() - 1, y2: y + 2);
879 }
880
881 // fade the invisible sections
882 const QRect top(grooveRect.x(),
883 grooveRect.y(),
884 grooveRect.width(),
885 visibleRect.y() - grooveRect.y() // Pen width
886 );
887 const QRect bottom(grooveRect.x(),
888 grooveRect.y() + visibleRect.y() + visibleRect.height() - grooveRect.y(), // Pen width
889 grooveRect.width(),
890 grooveRect.height() - (visibleRect.y() - grooveRect.y()) - visibleRect.height());
891
892 QColor faded(backgroundColor);
893 faded.setAlpha(110);
894 painter.fillRect(top, color: faded);
895 painter.fillRect(bottom, color: faded);
896
897 // add a thin line to limit the scrollbar
898 QColor c(foregroundColor);
899 c.setAlpha(10);
900 painter.setPen(QPen(c, 1));
901 painter.drawLine(x1: 0, y1: 0, x2: 0, y2: height());
902
903 if (m_showMarks) {
904 QHashIterator<int, QColor> it = m_lines;
905 QPen penBg;
906 penBg.setWidth(4);
907 lightShieldColor.setAlpha(180);
908 penBg.setColor(lightShieldColor);
909 painter.setPen(penBg);
910 while (it.hasNext()) {
911 it.next();
912 int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top();
913 painter.drawLine(x1: 6, y1: y, x2: width() - 6, y2: y);
914 }
915
916 it = m_lines;
917 QPen pen;
918 pen.setWidth(2);
919 while (it.hasNext()) {
920 it.next();
921 pen.setColor(it.value());
922 painter.setPen(pen);
923 int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top();
924 painter.drawLine(x1: 6, y1: y, x2: width() - 6, y2: y);
925 }
926 }
927
928 // slider outline
929 QColor sliderColor(highlightColor);
930 sliderColor.setAlpha(50);
931 painter.fillRect(sliderRect, color: sliderColor);
932 painter.setPen(QPen(highlightColor, 0));
933 m_sliderRect = sliderRect;
934 // rounded rect looks ugly for some reason, so we draw 4 lines.
935 painter.drawLine(x1: sliderRect.left(), y1: sliderRect.top() + 1, x2: sliderRect.left(), y2: sliderRect.bottom() - 1);
936 painter.drawLine(x1: sliderRect.right(), y1: sliderRect.top() + 1, x2: sliderRect.right(), y2: sliderRect.bottom() - 1);
937 painter.drawLine(x1: sliderRect.left() + 1, y1: sliderRect.top(), x2: sliderRect.right() - 1, y2: sliderRect.top());
938 painter.drawLine(x1: sliderRect.left() + 1, y1: sliderRect.bottom(), x2: sliderRect.right() - 1, y2: sliderRect.bottom());
939}
940
941void KateScrollBar::normalPaintEvent(QPaintEvent *e)
942{
943 QScrollBar::paintEvent(e);
944
945 if (!m_showMarks) {
946 return;
947 }
948
949 QPainter painter(this);
950
951 QStyleOptionSlider opt;
952 opt.initFrom(w: this);
953 opt.subControls = QStyle::SC_None;
954 opt.activeSubControls = QStyle::SC_None;
955 opt.orientation = orientation();
956 opt.minimum = minimum();
957 opt.maximum = maximum();
958 opt.sliderPosition = sliderPosition();
959 opt.sliderValue = value();
960 opt.singleStep = singleStep();
961 opt.pageStep = pageStep();
962
963 QRect rect = style()->subControlRect(cc: QStyle::CC_ScrollBar, opt: &opt, sc: QStyle::SC_ScrollBarSlider, widget: this);
964 int sideMargin = width() - rect.width();
965 if (sideMargin < 4) {
966 sideMargin = 4;
967 }
968 sideMargin /= 2;
969
970 QHashIterator<int, QColor> it = m_lines;
971 while (it.hasNext()) {
972 it.next();
973 painter.setPen(it.value());
974 if (it.key() < rect.top() || it.key() > rect.bottom()) {
975 painter.drawLine(x1: 0, y1: it.key(), x2: width(), y2: it.key());
976 } else {
977 painter.drawLine(x1: 0, y1: it.key(), x2: sideMargin, y2: it.key());
978 painter.drawLine(x1: width() - sideMargin, y1: it.key(), x2: width(), y2: it.key());
979 }
980 }
981}
982
983void KateScrollBar::resizeEvent(QResizeEvent *e)
984{
985 QScrollBar::resizeEvent(event: e);
986 m_updateTimer.start();
987 m_lines.clear();
988 update();
989}
990
991void KateScrollBar::sliderChange(SliderChange change)
992{
993 // call parents implementation
994 QScrollBar::sliderChange(change);
995
996 if (change == QAbstractSlider::SliderValueChange) {
997 redrawMarks();
998 } else if (change == QAbstractSlider::SliderRangeChange) {
999 marksChanged();
1000 }
1001
1002 if (m_leftMouseDown || m_middleMouseDown) {
1003 const int fromLine = m_viewInternal->toRealCursor(virtualCursor: m_viewInternal->startPos()).line() + 1;
1004 const int lastLine = m_viewInternal->toRealCursor(virtualCursor: m_viewInternal->endPos()).line() + 1;
1005 const QString text = i18nc("from line - to line", "<center>%1<br/>&#x2014;<br/>%2</center>", fromLine, lastLine);
1006 m_tooltipLineNoInfo.setText(text);
1007 m_tooltipLineNoInfo.setVisible(true);
1008 m_tooltipLineNoInfo.adjustSize();
1009 m_tooltipLineNoInfo.move(mapFromGlobal(m_toolTipPos));
1010 }
1011}
1012
1013#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
1014void KateScrollBar::contextMenuEvent(QContextMenuEvent *e)
1015{
1016 QMenu *menu = createStandardContextMenu(e->pos());
1017 menu->setAttribute(Qt::WA_DeleteOnClose);
1018
1019 QAction *firstStandardAction = menu->actions().first();
1020
1021 KActionCollection *ac = m_view->actionCollection();
1022
1023 if (QAction *scrollBarMarks = ac->action(QStringLiteral("view_scrollbar_marks"))) {
1024 menu->insertAction(firstStandardAction, scrollBarMarks);
1025 }
1026 if (QAction *scrollBarMinimap = ac->action(QStringLiteral("view_scrollbar_minimap"))) {
1027 menu->insertAction(firstStandardAction, scrollBarMinimap);
1028 }
1029 menu->insertSeparator(firstStandardAction);
1030
1031 menu->popup(e->globalPos());
1032}
1033#endif
1034
1035void KateScrollBar::marksChanged()
1036{
1037 m_lines.clear();
1038 update();
1039}
1040
1041void KateScrollBar::redrawMarks()
1042{
1043 if (!m_showMarks) {
1044 return;
1045 }
1046 update();
1047}
1048
1049void KateScrollBar::recomputeMarksPositions()
1050{
1051 // get the style options to compute the scrollbar pixels
1052 QStyleOptionSlider opt;
1053 initStyleOption(option: &opt);
1054 QRect grooveRect = style()->subControlRect(cc: QStyle::CC_ScrollBar, opt: &opt, sc: QStyle::SC_ScrollBarGroove, widget: this);
1055
1056 // cache top margin and groove height
1057 const int top = grooveRect.top();
1058 const int h = grooveRect.height() - 1;
1059
1060 // make sure we have a sane height
1061 if (h <= 0) {
1062 return;
1063 }
1064
1065 // get total visible (=without folded) lines in the document
1066 int visibleLines = m_view->textFolding().visibleLines() - 1;
1067 if (m_view->config()->scrollPastEnd()) {
1068 visibleLines += m_viewInternal->linesDisplayed() - 1;
1069 visibleLines -= m_view->config()->autoCenterLines();
1070 }
1071
1072 const QColor searchMatchColor = m_view->defaultStyleAttribute(defaultStyle: KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color();
1073
1074 // now repopulate the scrollbar lines list
1075 m_lines.clear();
1076 const QHash<int, KTextEditor::Mark *> &marks = m_doc->marks();
1077 for (QHash<int, KTextEditor::Mark *>::const_iterator i = marks.constBegin(); i != marks.constEnd(); ++i) {
1078 KTextEditor::Mark *mark = i.value();
1079 const int line = m_view->textFolding().lineToVisibleLine(line: mark->line);
1080 const double ratio = static_cast<double>(line) / visibleLines;
1081 const QColor markColor = mark->type == KTextEditor::Document::SearchMatch
1082 ? searchMatchColor
1083 : m_view->rendererConfig()->lineMarkerColor(type: (KTextEditor::Document::MarkTypes)mark->type);
1084 m_lines.insert(key: top + (int)(h * ratio), value: markColor);
1085 }
1086}
1087
1088void KateScrollBar::sliderMaybeMoved(int value)
1089{
1090 if (m_middleMouseDown) {
1091 // we only need to emit this signal once, as for the following slider
1092 // movements the signal sliderMoved() is already emitted.
1093 // Thus, set m_middleMouseDown to false right away.
1094 m_middleMouseDown = false;
1095 Q_EMIT sliderMMBMoved(value);
1096 }
1097}
1098// END
1099
1100// BEGIN KateCmdLineEditFlagCompletion
1101/**
1102 * This class provide completion of flags. It shows a short description of
1103 * each flag, and flags are appended.
1104 */
1105class KateCmdLineEditFlagCompletion : public KCompletion
1106{
1107public:
1108 KateCmdLineEditFlagCompletion()
1109 {
1110 ;
1111 }
1112
1113 QString makeCompletion(const QString & /*s*/) override
1114 {
1115 return QString();
1116 }
1117};
1118// END KateCmdLineEditFlagCompletion
1119
1120// BEGIN KateCmdLineEdit
1121KateCommandLineBar::KateCommandLineBar(KTextEditor::ViewPrivate *view, QWidget *parent)
1122 : KateViewBarWidget(true, parent)
1123{
1124 QHBoxLayout *topLayout = new QHBoxLayout(centralWidget());
1125 topLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
1126 m_lineEdit = new KateCmdLineEdit(this, view);
1127 connect(sender: m_lineEdit, signal: &KateCmdLineEdit::hideRequested, context: this, slot: &KateCommandLineBar::hideMe);
1128 topLayout->addWidget(m_lineEdit);
1129
1130 QToolButton *helpButton = new QToolButton(this);
1131 helpButton->setAutoRaise(true);
1132 helpButton->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual")));
1133 topLayout->addWidget(helpButton);
1134 connect(sender: helpButton, signal: &QToolButton::clicked, context: this, slot: &KateCommandLineBar::showHelpPage);
1135
1136 setFocusProxy(m_lineEdit);
1137}
1138
1139void KateCommandLineBar::showHelpPage()
1140{
1141 KHelpClient::invokeHelp(QStringLiteral("advanced-editing-tools-commandline"), QStringLiteral("katepart"));
1142}
1143
1144KateCommandLineBar::~KateCommandLineBar() = default;
1145
1146// inserts the given string in the command line edit and (if selected = true) selects it so the user
1147// can type over it if they want to
1148void KateCommandLineBar::setText(const QString &text, bool selected)
1149{
1150 m_lineEdit->setText(text);
1151 if (selected) {
1152 m_lineEdit->selectAll();
1153 }
1154}
1155
1156void KateCommandLineBar::execute(const QString &text)
1157{
1158 m_lineEdit->slotReturnPressed(cmd: text);
1159}
1160
1161KateCmdLineEdit::KateCmdLineEdit(KateCommandLineBar *bar, KTextEditor::ViewPrivate *view)
1162 : KLineEdit()
1163 , m_view(view)
1164 , m_bar(bar)
1165 , m_msgMode(false)
1166 , m_histpos(0)
1167 , m_cmdend(0)
1168 , m_command(nullptr)
1169{
1170 connect(sender: this, signal: &KateCmdLineEdit::returnKeyPressed, context: this, slot: &KateCmdLineEdit::slotReturnPressed);
1171
1172 setCompletionObject(KateCmd::self()->commandCompletionObject());
1173 setAutoDeleteCompletionObject(false);
1174
1175 m_hideTimer = new QTimer(this);
1176 m_hideTimer->setSingleShot(true);
1177 connect(sender: m_hideTimer, signal: &QTimer::timeout, context: this, slot: &KateCmdLineEdit::hideLineEdit);
1178
1179 // make sure the timer is stopped when the user switches views. if not, focus will be given to the
1180 // wrong view when KateViewBar::hideCurrentBarWidget() is called after 4 seconds. (the timer is
1181 // used for showing things like "Success" for four seconds after the user has used the kate
1182 // command line)
1183 connect(sender: m_view, signal: &KTextEditor::ViewPrivate::focusOut, context: m_hideTimer, slot: &QTimer::stop);
1184}
1185
1186void KateCmdLineEdit::hideEvent(QHideEvent *e)
1187{
1188 Q_UNUSED(e);
1189}
1190
1191QString KateCmdLineEdit::helptext(const QPoint &) const
1192{
1193 const QString beg = QStringLiteral("<qt background=\"white\"><div><table width=\"100%\"><tr><td bgcolor=\"brown\"><font color=\"white\"><b>Help: <big>");
1194 const QString mid = QStringLiteral("</big></b></font></td></tr><tr><td>");
1195 const QString end = QStringLiteral("</td></tr></table></div><qt>");
1196
1197 const QString t = text();
1198 static const QRegularExpression re(QStringLiteral("\\s*help\\s+(.*)"));
1199 auto match = re.match(subject: t);
1200 if (match.hasMatch()) {
1201 QString s;
1202 // get help for command
1203 const QString name = match.captured(nth: 1);
1204 if (name == QLatin1String("list")) {
1205 return beg + i18n("Available Commands") + mid + KateCmd::self()->commandList().join(sep: QLatin1Char(' '))
1206 + i18n("<p>For help on individual commands, do <code>'help &lt;command&gt;'</code></p>") + end;
1207 } else if (!name.isEmpty()) {
1208 KTextEditor::Command *cmd = KateCmd::self()->queryCommand(cmd: name);
1209 if (cmd) {
1210 if (cmd->help(view: m_view, cmd: name, msg&: s)) {
1211 return beg + name + mid + s + end;
1212 } else {
1213 return beg + name + mid + i18n("No help for '%1'", name) + end;
1214 }
1215 } else {
1216 return beg + mid + i18n("No such command <b>%1</b>", name) + end;
1217 }
1218 }
1219 }
1220
1221 return beg + mid
1222 + i18n("<p>This is the Katepart <b>command line</b>.<br />"
1223 "Syntax: <code><b>command [ arguments ]</b></code><br />"
1224 "For a list of available commands, enter <code><b>help list</b></code><br />"
1225 "For help for individual commands, enter <code><b>help &lt;command&gt;</b></code></p>")
1226 + end;
1227}
1228
1229bool KateCmdLineEdit::event(QEvent *e)
1230{
1231 if (e->type() == QEvent::QueryWhatsThis) {
1232 setWhatsThis(helptext(QPoint()));
1233 e->accept();
1234 return true;
1235 }
1236 return KLineEdit::event(e);
1237}
1238
1239/**
1240 * Parse the text as a command.
1241 *
1242 * The following is a simple PEG grammar for the syntax of the command.
1243 * (A PEG grammar is like a BNF grammar, except that "/" stands for
1244 * ordered choice: only the first matching rule is used. In other words,
1245 * the parsing is short-circuited in the manner of the "or" operator in
1246 * programming languages, and so the grammar is unambiguous.)
1247 *
1248 * Text <- Range? Command
1249 * / Position
1250 * Range <- Position ("," Position)?
1251 * / "%"
1252 * Position <- Base Offset?
1253 * Base <- Line
1254 * / LastLine
1255 * / ThisLine
1256 * / Mark
1257 * Offset <- [+-] Base
1258 * Line <- [0-9]+
1259 * LastLine <- "$"
1260 * ThisLine <- "."
1261 * Mark <- "'" [a-z]
1262 */
1263
1264void KateCmdLineEdit::slotReturnPressed(const QString &text)
1265{
1266 static const QRegularExpression focusChangingCommands(QStringLiteral("^(?:buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$"));
1267
1268 if (text.isEmpty()) {
1269 return;
1270 }
1271 // silently ignore leading space characters
1272 uint n = 0;
1273 const uint textlen = text.length();
1274 while ((n < textlen) && (text[n].isSpace())) {
1275 n++;
1276 }
1277
1278 if (n >= textlen) {
1279 return;
1280 }
1281
1282 QString cmd = text.mid(position: n);
1283
1284 // Parse any leading range expression, and strip it (and maybe do some other transforms on the command).
1285 QString leadingRangeExpression;
1286 KTextEditor::Range range = CommandRangeExpressionParser::parseRangeExpression(command: cmd, view: m_view, destRangeExpression&: leadingRangeExpression, destTransformedCommand&: cmd);
1287
1288 // Built in help: if the command starts with "help", [try to] show some help
1289 if (cmd.startsWith(s: QLatin1String("help"))) {
1290 QWhatsThis::showText(pos: mapToGlobal(QPoint(0, 0)), text: helptext(QPoint()));
1291 clear();
1292 KateCmd::self()->appendHistory(cmd);
1293 m_histpos = KateCmd::self()->historyLength();
1294 m_oldText.clear();
1295 return;
1296 }
1297
1298 if (cmd.length() > 0) {
1299 KTextEditor::Command *p = KateCmd::self()->queryCommand(cmd);
1300
1301 m_oldText = leadingRangeExpression + cmd;
1302 m_msgMode = true;
1303
1304 // if the command changes the focus itself, the bar should be hidden before execution.
1305 if (focusChangingCommands.matchView(subjectView: QStringView(cmd).left(n: cmd.indexOf(ch: QLatin1Char(' ')))).hasMatch()) {
1306 Q_EMIT hideRequested();
1307 }
1308
1309 if (!p) {
1310 setText(i18n("No such command: \"%1\"", cmd));
1311 } else if (range.isValid() && !p->supportsRange(cmd)) {
1312 // Raise message, when the command does not support ranges.
1313 setText(i18n("Error: No range allowed for command \"%1\".", cmd));
1314 } else {
1315 QString msg;
1316 if (p->exec(view: m_view, cmd, msg, range)) {
1317 // append command along with range (will be empty if none given) to history
1318 KateCmd::self()->appendHistory(cmd: leadingRangeExpression + cmd);
1319 m_histpos = KateCmd::self()->historyLength();
1320 m_oldText.clear();
1321
1322 if (msg.length() > 0) {
1323 setText(i18n("Success: ") + msg);
1324 } else if (isVisible()) {
1325 // always hide on success without message
1326 Q_EMIT hideRequested();
1327 }
1328 } else {
1329 if (msg.length() > 0) {
1330 if (msg.contains(c: QLatin1Char('\n'))) {
1331 // multiline error, use widget with more space
1332 QWhatsThis::showText(pos: mapToGlobal(QPoint(0, 0)), text: msg);
1333 } else {
1334 setText(msg);
1335 }
1336 } else {
1337 setText(i18n("Command \"%1\" failed.", cmd));
1338 }
1339 }
1340 }
1341 }
1342
1343 // clean up
1344 if (completionObject() != KateCmd::self()->commandCompletionObject()) {
1345 KCompletion *c = completionObject();
1346 setCompletionObject(KateCmd::self()->commandCompletionObject());
1347 delete c;
1348 }
1349 m_command = nullptr;
1350 m_cmdend = 0;
1351
1352 if (!focusChangingCommands.matchView(subjectView: QStringView(cmd).left(n: cmd.indexOf(ch: QLatin1Char(' ')))).hasMatch()) {
1353 m_view->setFocus();
1354 }
1355
1356 if (isVisible()) {
1357 m_hideTimer->start(msec: 4000);
1358 }
1359}
1360
1361void KateCmdLineEdit::hideLineEdit() // unless i have focus ;)
1362{
1363 if (!hasFocus()) {
1364 Q_EMIT hideRequested();
1365 }
1366}
1367
1368void KateCmdLineEdit::focusInEvent(QFocusEvent *ev)
1369{
1370 if (m_msgMode) {
1371 m_msgMode = false;
1372 setText(m_oldText);
1373 selectAll();
1374 }
1375
1376 KLineEdit::focusInEvent(ev);
1377}
1378
1379void KateCmdLineEdit::keyPressEvent(QKeyEvent *ev)
1380{
1381 if (ev->key() == Qt::Key_Escape || (ev->key() == Qt::Key_BracketLeft && ev->modifiers() == Qt::ControlModifier)) {
1382 m_view->setFocus();
1383 hideLineEdit();
1384 clear();
1385 } else if (ev->key() == Qt::Key_Up) {
1386 fromHistory(up: true);
1387 } else if (ev->key() == Qt::Key_Down) {
1388 fromHistory(up: false);
1389 }
1390
1391 uint cursorpos = cursorPosition();
1392 KLineEdit::keyPressEvent(ev);
1393
1394 // during typing, let us see if we have a valid command
1395 if (!m_cmdend || cursorpos <= m_cmdend) {
1396 QChar c;
1397 if (!ev->text().isEmpty()) {
1398 c = ev->text().at(i: 0);
1399 }
1400
1401 if (!m_cmdend && !c.isNull()) { // we have no command, so lets see if we got one
1402 if (!c.isLetterOrNumber() && c != QLatin1Char('-') && c != QLatin1Char('_')) {
1403 m_command = KateCmd::self()->queryCommand(cmd: text().trimmed());
1404 if (m_command) {
1405 // qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<<m_command<<". text is '"<<text()<<"'";
1406 // if the typed character is ":",
1407 // we try if the command has flag completions
1408 m_cmdend = cursorpos;
1409 // qCDebug(LOG_KTE)<<"keypress in commandline: Set m_cmdend to "<<m_cmdend;
1410 } else {
1411 m_cmdend = 0;
1412 }
1413 }
1414 } else { // since cursor is inside the command name, we reconsider it
1415 // qCDebug(LOG_KTE) << "keypress in commandline: \\W -- text is " << text();
1416 m_command = KateCmd::self()->queryCommand(cmd: text().trimmed());
1417 if (m_command) {
1418 // qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<<m_command;
1419 QString t = text();
1420 m_cmdend = 0;
1421 bool b = false;
1422 for (; (int)m_cmdend < t.length(); m_cmdend++) {
1423 if (t[m_cmdend].isLetter()) {
1424 b = true;
1425 }
1426 if (b && (!t[m_cmdend].isLetterOrNumber() && t[m_cmdend] != QLatin1Char('-') && t[m_cmdend] != QLatin1Char('_'))) {
1427 break;
1428 }
1429 }
1430
1431 if (c == QLatin1Char(':') && cursorpos == m_cmdend) {
1432 // check if this command wants to complete flags
1433 // qCDebug(LOG_KTE)<<"keypress in commandline: Checking if flag completion is desired!";
1434 }
1435 } else {
1436 // clean up if needed
1437 if (completionObject() != KateCmd::self()->commandCompletionObject()) {
1438 KCompletion *c = completionObject();
1439 setCompletionObject(KateCmd::self()->commandCompletionObject());
1440 delete c;
1441 }
1442
1443 m_cmdend = 0;
1444 }
1445 }
1446
1447 // if we got a command, check if it wants to do something.
1448 if (m_command) {
1449 KCompletion *cmpl = m_command->completionObject(view: m_view, cmdname: text().left(n: m_cmdend).trimmed());
1450 if (cmpl) {
1451 // We need to prepend the current command name + flag string
1452 // when completion is done
1453 // qCDebug(LOG_KTE)<<"keypress in commandline: Setting completion object!";
1454
1455 setCompletionObject(cmpl);
1456 }
1457 }
1458 } else if (m_command && !ev->text().isEmpty()) {
1459 // check if we should call the commands processText()
1460 if (m_command->wantsToProcessText(cmdname: text().left(n: m_cmdend).trimmed())) {
1461 m_command->processText(view: m_view, text: text());
1462 }
1463 }
1464}
1465
1466void KateCmdLineEdit::fromHistory(bool up)
1467{
1468 if (!KateCmd::self()->historyLength()) {
1469 return;
1470 }
1471
1472 QString s;
1473
1474 if (up) {
1475 if (m_histpos > 0) {
1476 m_histpos--;
1477 s = KateCmd::self()->fromHistory(i: m_histpos);
1478 }
1479 } else {
1480 if (m_histpos < (KateCmd::self()->historyLength() - 1)) {
1481 m_histpos++;
1482 s = KateCmd::self()->fromHistory(i: m_histpos);
1483 } else {
1484 m_histpos = KateCmd::self()->historyLength();
1485 setText(m_oldText);
1486 }
1487 }
1488 if (!s.isEmpty()) {
1489 // Select the argument part of the command, so that it is easy to overwrite
1490 setText(s);
1491 static const QRegularExpression reCmd(QStringLiteral("^[\\w\\-]+(?:[^a-zA-Z0-9_-]|:\\w+)(.*)"), QRegularExpression::UseUnicodePropertiesOption);
1492 const auto match = reCmd.match(subject: text());
1493 if (match.hasMatch()) {
1494 setSelection(text().length() - match.capturedLength(nth: 1), match.capturedLength(nth: 1));
1495 }
1496 }
1497}
1498// END KateCmdLineEdit
1499
1500// BEGIN KateIconBorder
1501using namespace KTextEditor;
1502
1503KateIconBorder::KateIconBorder(KateViewInternal *internalView, QWidget *parent)
1504 : QWidget(parent)
1505 , m_view(internalView->m_view)
1506 , m_doc(internalView->doc())
1507 , m_viewInternal(internalView)
1508 , m_iconBorderOn(false)
1509 , m_lineNumbersOn(false)
1510 , m_relLineNumbersOn(false)
1511 , m_updateRelLineNumbers(false)
1512 , m_foldingMarkersOn(false)
1513 , m_dynWrapIndicatorsOn(false)
1514 , m_annotationBorderOn(false)
1515 , m_updatePositionToArea(true)
1516 , m_annotationItemDelegate(new KateAnnotationItemDelegate(this))
1517{
1518 setAcceptDrops(true);
1519 setAttribute(Qt::WA_StaticContents);
1520
1521 // See: https://doc.qt.io/qt-5/qwidget.html#update. As this widget does not
1522 // have a background, there's no need for Qt to erase the widget's area
1523 // before repainting. Enabling this prevents flickering when the widget is
1524 // repainted.
1525 setAttribute(Qt::WA_OpaquePaintEvent);
1526
1527 setSizePolicy(hor: QSizePolicy::Fixed, ver: QSizePolicy::Minimum);
1528 setMouseTracking(true);
1529 m_doc->setMarkDescription(Document::markType01, i18n("Bookmark"));
1530 m_doc->setMarkIcon(markType: Document::markType01, icon: QIcon::fromTheme(QStringLiteral("bookmarks")));
1531
1532 connect(sender: m_annotationItemDelegate, signal: &AbstractAnnotationItemDelegate::sizeHintChanged, context: this, slot: &KateIconBorder::updateAnnotationBorderWidth);
1533
1534 updateFont();
1535
1536 m_antiFlickerTimer.setSingleShot(true);
1537 m_antiFlickerTimer.setInterval(300);
1538 connect(sender: &m_antiFlickerTimer, signal: &QTimer::timeout, context: this, slot: &KateIconBorder::highlightFolding);
1539
1540 // user interaction (scrolling) hides e.g. preview
1541 connect(sender: m_view, signal: &KTextEditor::View::displayRangeChanged, context: this, slot: &KateIconBorder::displayRangeChanged);
1542}
1543
1544KateIconBorder::~KateIconBorder()
1545{
1546 delete m_foldingPreview;
1547 delete m_foldingRange;
1548}
1549
1550void KateIconBorder::setIconBorderOn(bool enable)
1551{
1552 if (enable == m_iconBorderOn) {
1553 return;
1554 }
1555
1556 m_iconBorderOn = enable;
1557
1558 m_updatePositionToArea = true;
1559
1560 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
1561}
1562
1563void KateIconBorder::setAnnotationBorderOn(bool enable)
1564{
1565 if (enable == m_annotationBorderOn) {
1566 return;
1567 }
1568
1569 m_annotationBorderOn = enable;
1570
1571 // make sure the tooltip is hidden
1572 if (!m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) {
1573 m_hoveredAnnotationGroupIdentifier.clear();
1574 hideAnnotationTooltip();
1575 }
1576
1577 Q_EMIT m_view->annotationBorderVisibilityChanged(view: m_view, visible: enable);
1578
1579 m_updatePositionToArea = true;
1580
1581 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
1582}
1583
1584void KateIconBorder::removeAnnotationHovering()
1585{
1586 // remove hovering if it's still there
1587 if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) {
1588 m_hoveredAnnotationGroupIdentifier.clear();
1589 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
1590 }
1591}
1592
1593void KateIconBorder::setLineNumbersOn(bool enable)
1594{
1595 if (enable == m_lineNumbersOn) {
1596 return;
1597 }
1598
1599 m_lineNumbersOn = enable;
1600 m_dynWrapIndicatorsOn = (m_dynWrapIndicators == 1) ? enable : m_dynWrapIndicators;
1601
1602 m_updatePositionToArea = true;
1603
1604 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
1605}
1606
1607void KateIconBorder::setRelLineNumbersOn(bool enable)
1608{
1609 if (enable == m_relLineNumbersOn) {
1610 return;
1611 }
1612
1613 m_relLineNumbersOn = enable;
1614 /*
1615 * We don't have to touch the m_dynWrapIndicatorsOn because
1616 * we already got it right from the m_lineNumbersOn
1617 */
1618 m_updatePositionToArea = true;
1619
1620 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
1621}
1622
1623void KateIconBorder::updateForCursorLineChange()
1624{
1625 if (m_relLineNumbersOn) {
1626 m_updateRelLineNumbers = true;
1627 }
1628
1629 // always do normal update, e.g. for different current line color!
1630 update();
1631}
1632
1633void KateIconBorder::setDynWrapIndicators(int state)
1634{
1635 if (state == m_dynWrapIndicators) {
1636 return;
1637 }
1638
1639 m_dynWrapIndicators = state;
1640 m_dynWrapIndicatorsOn = (state == 1) ? m_lineNumbersOn : state;
1641
1642 m_updatePositionToArea = true;
1643
1644 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
1645}
1646
1647void KateIconBorder::setFoldingMarkersOn(bool enable)
1648{
1649 if (enable == m_foldingMarkersOn) {
1650 return;
1651 }
1652
1653 m_foldingMarkersOn = enable;
1654
1655 m_updatePositionToArea = true;
1656
1657 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
1658}
1659
1660QSize KateIconBorder::sizeHint() const
1661{
1662 int w = 1; // Must be any value != 0 or we will never painted!
1663
1664 const int i = m_positionToArea.size();
1665 if (i > 0) {
1666 w = m_positionToArea.at(n: i - 1).first;
1667 }
1668
1669 return QSize(w, 0);
1670}
1671
1672// This function (re)calculates the maximum width of any of the digit characters (0 -> 9)
1673// for graceful handling of variable-width fonts as the linenumber font.
1674void KateIconBorder::updateFont()
1675{
1676 // Loop to determine the widest numeric character in the current font.
1677 const QFontMetricsF &fm = m_view->renderer()->currentFontMetrics();
1678 m_maxCharWidth = 0.0;
1679 for (char c = '0'; c <= '9'; ++c) {
1680 const qreal charWidth = ceil(x: fm.horizontalAdvance(QLatin1Char(c)));
1681 m_maxCharWidth = qMax(a: m_maxCharWidth, b: charWidth);
1682 }
1683
1684 // NOTE/TODO(or not) Take size of m_dynWrapIndicatorChar into account.
1685 // It's a multi-char and it's reported size is, even with a mono-space font,
1686 // bigger than each digit, e.g. 10 vs 12. Currently it seems to work even with
1687 // "Line Numbers Off" but all these width calculating looks slightly hacky
1688
1689 // the icon pane scales with the font...
1690 m_iconAreaWidth = fm.height();
1691
1692 // Only for now, later may that become an own value
1693 m_foldingAreaWidth = m_iconAreaWidth;
1694
1695 calcAnnotationBorderWidth();
1696
1697 m_updatePositionToArea = true;
1698
1699 QMetaObject::invokeMethod(
1700 object: this,
1701 function: [this] {
1702 update();
1703 },
1704 type: Qt::QueuedConnection);
1705}
1706
1707int KateIconBorder::lineNumberWidth() const
1708{
1709 int width = 0;
1710 // Avoid unneeded expensive calculations ;-)
1711 if (m_lineNumbersOn) {
1712 // width = (number of digits + 1) * char width
1713 const int digits = (int)ceil(x: log10(x: (double)(m_view->doc()->lines() + 1)));
1714 width = (int)ceil(x: (digits + 1) * m_maxCharWidth);
1715 }
1716
1717 if ((width < 1) && m_dynWrapIndicatorsOn && m_view->config()->dynWordWrap()) {
1718 // FIXME Why 2x? because of above (number of digits + 1)
1719 // -> looks to me like a hint for bad calculation elsewhere
1720 width = 2 * m_maxCharWidth;
1721 }
1722
1723 return width;
1724}
1725
1726void KateIconBorder::dragEnterEvent(QDragEnterEvent *event)
1727{
1728 m_view->m_viewInternal->dragEnterEvent(event);
1729}
1730
1731void KateIconBorder::dragMoveEvent(QDragMoveEvent *event)
1732{
1733 // FIXME Just calling m_view->m_viewInternal->dragMoveEvent(e) don't work
1734 // as intended, we need to set the cursor at column 1
1735 // Is there a way to change the pos of the event?
1736 QPoint pos(0, event->position().y());
1737 // Code copy of KateViewInternal::dragMoveEvent
1738 m_view->m_viewInternal->placeCursor(p: pos, keepSelection: true, updateSelection: false);
1739 m_view->m_viewInternal->fixDropEvent(event);
1740}
1741
1742void KateIconBorder::dropEvent(QDropEvent *event)
1743{
1744 m_view->m_viewInternal->dropEvent(event);
1745}
1746
1747void KateIconBorder::paintEvent(QPaintEvent *e)
1748{
1749 paintBorder(x: e->rect().x(), y: e->rect().y(), width: e->rect().width(), height: e->rect().height());
1750}
1751
1752static void paintTriangle(QPainter &painter, QColor c, int xOffset, int yOffset, int width, int height, bool open)
1753{
1754 painter.setRenderHint(hint: QPainter::Antialiasing);
1755
1756 qreal size = qMin(a: width, b: height);
1757
1758 if (open) {
1759 // Paint unfolded icon less pushy
1760 if (KColorUtils::luma(c) < 0.25) {
1761 c = KColorUtils::darken(c);
1762 } else {
1763 c = KColorUtils::shade(c, lumaAmount: 0.1);
1764 }
1765
1766 } else {
1767 // Paint folded icon in contrast to popup highlighting
1768 if (KColorUtils::luma(c) > 0.25) {
1769 c = KColorUtils::darken(c);
1770 } else {
1771 c = KColorUtils::shade(c, lumaAmount: 0.1);
1772 }
1773 }
1774
1775 QPen pen;
1776 pen.setJoinStyle(Qt::RoundJoin);
1777 pen.setColor(c);
1778 pen.setWidthF(1.5);
1779 painter.setPen(pen);
1780 painter.setBrush(c);
1781
1782 // let some border, if possible
1783 size *= 0.6;
1784
1785 qreal halfSize = size / 2;
1786 qreal halfSizeP = halfSize * 0.6;
1787 QPointF middle(xOffset + (qreal)width / 2, yOffset + (qreal)height / 2);
1788
1789 if (open) {
1790 QPointF points[3] = {middle + QPointF(-halfSize, -halfSizeP), middle + QPointF(halfSize, -halfSizeP), middle + QPointF(0, halfSizeP)};
1791 painter.drawConvexPolygon(points, pointCount: 3);
1792 } else {
1793 QPointF points[3] = {middle + QPointF(-halfSizeP, -halfSize), middle + QPointF(-halfSizeP, halfSize), middle + QPointF(halfSizeP, 0)};
1794 painter.drawConvexPolygon(points, pointCount: 3);
1795 }
1796
1797 painter.setRenderHint(hint: QPainter::Antialiasing, on: false);
1798}
1799
1800/**
1801 * Helper class for an identifier which can be an empty or non-empty string or invalid.
1802 * Avoids complicated explicit statements in code dealing with the identifier
1803 * received as QVariant from a model.
1804 */
1805class KateAnnotationGroupIdentifier
1806{
1807public:
1808 KateAnnotationGroupIdentifier(const QVariant &identifier)
1809 : m_isValid(identifier.isValid() && identifier.canConvert<QString>())
1810 , m_id(m_isValid ? identifier.toString() : QString())
1811 {
1812 }
1813 KateAnnotationGroupIdentifier() = default;
1814 KateAnnotationGroupIdentifier(const KateAnnotationGroupIdentifier &rhs) = default;
1815
1816 KateAnnotationGroupIdentifier &operator=(const KateAnnotationGroupIdentifier &rhs)
1817 {
1818 m_isValid = rhs.m_isValid;
1819 m_id = rhs.m_id;
1820 return *this;
1821 }
1822 KateAnnotationGroupIdentifier &operator=(const QVariant &identifier)
1823 {
1824 m_isValid = (identifier.isValid() && identifier.canConvert<QString>());
1825 if (m_isValid) {
1826 m_id = identifier.toString();
1827 } else {
1828 m_id.clear();
1829 }
1830 return *this;
1831 }
1832
1833 bool operator==(const KateAnnotationGroupIdentifier &rhs) const
1834 {
1835 return (m_isValid == rhs.m_isValid) && (!m_isValid || (m_id == rhs.m_id));
1836 }
1837 bool operator!=(const KateAnnotationGroupIdentifier &rhs) const
1838 {
1839 return (m_isValid != rhs.m_isValid) || (m_isValid && (m_id != rhs.m_id));
1840 }
1841
1842 void clear()
1843 {
1844 m_isValid = false;
1845 m_id.clear();
1846 }
1847 bool isValid() const
1848 {
1849 return m_isValid;
1850 }
1851 const QString &id() const
1852 {
1853 return m_id;
1854 }
1855
1856private:
1857 bool m_isValid = false;
1858 QString m_id;
1859};
1860
1861/**
1862 * Helper class for iterative calculation of data regarding the position
1863 * of a line with regard to annotation item grouping.
1864 */
1865class KateAnnotationGroupPositionState
1866{
1867public:
1868 /**
1869 * @param startz first rendered displayed line
1870 * @param isUsed flag whether the KateAnnotationGroupPositionState object will
1871 * be used or is just created due to being on the stack
1872 */
1873 KateAnnotationGroupPositionState(KateViewInternal *viewInternal,
1874 const KTextEditor::AnnotationModel *model,
1875 const QString &hoveredAnnotationGroupIdentifier,
1876 uint startz,
1877 bool isUsed);
1878 /**
1879 * @param styleOption option to fill with data for the given line
1880 * @param z rendered displayed line
1881 * @param realLine real line which is rendered here (passed to avoid another look-up)
1882 */
1883 void nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine);
1884
1885private:
1886 KateViewInternal *m_viewInternal;
1887 const KTextEditor::AnnotationModel *const m_model;
1888 const QString m_hoveredAnnotationGroupIdentifier;
1889
1890 int m_visibleWrappedLineInAnnotationGroup = -1;
1891 KateAnnotationGroupIdentifier m_lastAnnotationGroupIdentifier;
1892 KateAnnotationGroupIdentifier m_nextAnnotationGroupIdentifier;
1893 bool m_isSameAnnotationGroupsSinceLast = false;
1894};
1895
1896KateAnnotationGroupPositionState::KateAnnotationGroupPositionState(KateViewInternal *viewInternal,
1897 const KTextEditor::AnnotationModel *model,
1898 const QString &hoveredAnnotationGroupIdentifier,
1899 uint startz,
1900 bool isUsed)
1901 : m_viewInternal(viewInternal)
1902 , m_model(model)
1903 , m_hoveredAnnotationGroupIdentifier(hoveredAnnotationGroupIdentifier)
1904{
1905 if (!isUsed) {
1906 return;
1907 }
1908
1909 if (!m_model || (static_cast<int>(startz) >= m_viewInternal->cache()->viewCacheLineCount())) {
1910 return;
1911 }
1912
1913 const auto realLineAtStart = m_viewInternal->cache()->viewLine(viewLine: startz).line();
1914 m_nextAnnotationGroupIdentifier = m_model->data(line: realLineAtStart, role: (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole);
1915 if (m_nextAnnotationGroupIdentifier.isValid()) {
1916 // estimate state of annotation group before first rendered line
1917 if (startz == 0) {
1918 if (realLineAtStart > 0) {
1919 // TODO: here we would want to scan until the next line that would be displayed,
1920 // to see if there are any group changes until then
1921 // for now simply taking neighbour line into account, not a grave bug on the first displayed line
1922 m_lastAnnotationGroupIdentifier = m_model->data(line: realLineAtStart - 1, role: (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole);
1923 m_isSameAnnotationGroupsSinceLast = (m_lastAnnotationGroupIdentifier == m_nextAnnotationGroupIdentifier);
1924 }
1925 } else {
1926 const auto realLineBeforeStart = m_viewInternal->cache()->viewLine(viewLine: startz - 1).line();
1927 m_lastAnnotationGroupIdentifier = m_model->data(line: realLineBeforeStart, role: (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole);
1928 if (m_lastAnnotationGroupIdentifier.isValid()) {
1929 if (m_lastAnnotationGroupIdentifier.id() == m_nextAnnotationGroupIdentifier.id()) {
1930 m_isSameAnnotationGroupsSinceLast = true;
1931 // estimate m_visibleWrappedLineInAnnotationGroup from lines before startz
1932 for (uint z = startz; z > 0; --z) {
1933 const auto realLine = m_viewInternal->cache()->viewLine(viewLine: z - 1).line();
1934 const KateAnnotationGroupIdentifier identifier =
1935 m_model->data(line: realLine, role: (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole);
1936 if (identifier != m_lastAnnotationGroupIdentifier) {
1937 break;
1938 }
1939 ++m_visibleWrappedLineInAnnotationGroup;
1940 }
1941 }
1942 }
1943 }
1944 }
1945}
1946
1947void KateAnnotationGroupPositionState::nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine)
1948{
1949 styleOption.wrappedLine = m_viewInternal->cache()->viewLine(viewLine: z).viewLine();
1950 styleOption.wrappedLineCount = m_viewInternal->cache()->viewLineCount(realLine);
1951
1952 // Estimate position in group
1953 const KateAnnotationGroupIdentifier annotationGroupIdentifier = m_nextAnnotationGroupIdentifier;
1954 bool isSameAnnotationGroupsSinceThis = false;
1955 // Calculate next line's group identifier
1956 // shortcut: assuming wrapped lines are always displayed together, test is simple
1957 if (styleOption.wrappedLine + 1 < styleOption.wrappedLineCount) {
1958 m_nextAnnotationGroupIdentifier = annotationGroupIdentifier;
1959 isSameAnnotationGroupsSinceThis = true;
1960 } else {
1961 if (static_cast<int>(z + 1) < m_viewInternal->cache()->viewCacheLineCount()) {
1962 const int realLineAfter = m_viewInternal->cache()->viewLine(viewLine: z + 1).line();
1963 // search for any realLine with a different group id, also the non-displayed
1964 int rl = realLine + 1;
1965 for (; rl <= realLineAfter; ++rl) {
1966 m_nextAnnotationGroupIdentifier = m_model->data(line: rl, role: (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole);
1967 if (!m_nextAnnotationGroupIdentifier.isValid() || (m_nextAnnotationGroupIdentifier.id() != annotationGroupIdentifier.id())) {
1968 break;
1969 }
1970 }
1971 isSameAnnotationGroupsSinceThis = (rl > realLineAfter);
1972 if (rl < realLineAfter) {
1973 m_nextAnnotationGroupIdentifier = m_model->data(line: realLineAfter, role: (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole);
1974 }
1975 } else {
1976 // TODO: check next line after display end
1977 m_nextAnnotationGroupIdentifier.clear();
1978 isSameAnnotationGroupsSinceThis = false;
1979 }
1980 }
1981
1982 if (annotationGroupIdentifier.isValid()) {
1983 if (m_hoveredAnnotationGroupIdentifier == annotationGroupIdentifier.id()) {
1984 styleOption.state |= QStyle::State_MouseOver;
1985 } else {
1986 styleOption.state &= ~QStyle::State_MouseOver;
1987 }
1988
1989 if (m_isSameAnnotationGroupsSinceLast) {
1990 ++m_visibleWrappedLineInAnnotationGroup;
1991 } else {
1992 m_visibleWrappedLineInAnnotationGroup = 0;
1993 }
1994
1995 styleOption.annotationItemGroupingPosition = StyleOptionAnnotationItem::InGroup;
1996 if (!m_isSameAnnotationGroupsSinceLast) {
1997 styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupBegin;
1998 }
1999 if (!isSameAnnotationGroupsSinceThis) {
2000 styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupEnd;
2001 }
2002 } else {
2003 m_visibleWrappedLineInAnnotationGroup = 0;
2004 }
2005 styleOption.visibleWrappedLineInGroup = m_visibleWrappedLineInAnnotationGroup;
2006
2007 m_lastAnnotationGroupIdentifier = m_nextAnnotationGroupIdentifier;
2008 m_isSameAnnotationGroupsSinceLast = isSameAnnotationGroupsSinceThis;
2009}
2010
2011void KateIconBorder::paintBorder(int /*x*/, int y, int /*width*/, int height)
2012{
2013 const uint h = m_view->renderer()->lineHeight();
2014 const uint startz = (y / h);
2015 const uint endz = qMin(a: startz + 1 + (height / h), b: static_cast<uint>(m_viewInternal->cache()->viewCacheLineCount()));
2016 const uint currentLine = m_view->cursorPosition().line();
2017
2018 // center the folding boxes
2019 int m_px = (h - 11) / 2;
2020 if (m_px < 0) {
2021 m_px = 0;
2022 }
2023
2024 // Ensure we miss no change of the count of line number digits
2025 const int newNeededWidth = lineNumberWidth();
2026
2027 if (m_updatePositionToArea || (newNeededWidth != m_lineNumberAreaWidth)) {
2028 m_lineNumberAreaWidth = newNeededWidth;
2029 m_updatePositionToArea = true;
2030 m_positionToArea.clear();
2031 }
2032
2033 QPainter p(this);
2034 p.setRenderHints(hints: QPainter::TextAntialiasing);
2035 p.setFont(m_view->renderer()->currentFont()); // for line numbers
2036
2037 KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel();
2038 KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, m_hoveredAnnotationGroupIdentifier, startz, m_annotationBorderOn);
2039
2040 // Fetch often used data only once, improve readability
2041 const int w = width();
2042 const QColor iconBarColor = m_view->rendererConfig()->iconBarColor(); // Effective our background
2043 const QColor lineNumberColor = m_view->rendererConfig()->lineNumberColor();
2044 const QColor backgroundColor = m_view->rendererConfig()->backgroundColor(); // Of the edit area
2045 const QColor currentLineHighlight = m_view->rendererConfig()->highlightedLineColor(); // Of the edit area
2046
2047 // Paint the border in chunks line by line
2048 for (uint z = startz; z < endz; z++) {
2049 // Painting coordinates, lineHeight * lineNumber
2050 const uint y = h * z;
2051
2052 // Paint the border in chunks left->right, remember used width
2053 uint lnX = 0;
2054
2055 // get line for this coordinates if possible
2056 const KateTextLayout lineLayout = m_viewInternal->cache()->viewLine(viewLine: z);
2057 const int realLine = lineLayout.line();
2058
2059 // Paint background over full width
2060 p.fillRect(x: lnX, y, w, h, b: iconBarColor);
2061
2062 // overpaint with current line highlighting over full width
2063 const bool isCurrentLine = (realLine == static_cast<int>(currentLine)) && lineLayout.includesCursor(realCursor: m_view->cursorPosition());
2064 if (isCurrentLine) {
2065 p.fillRect(x: lnX, y, w, h, b: currentLineHighlight);
2066 }
2067
2068 // for real lines we need to do more stuff ;=)
2069 if (realLine >= 0) {
2070 // icon pane
2071 if (m_iconBorderOn) {
2072 const uint mrk(m_doc->mark(line: realLine)); // call only once
2073 if (mrk && lineLayout.startCol() == 0) {
2074 for (uint bit = 0; bit < 32; bit++) {
2075 Document::MarkTypes markType = (Document::MarkTypes)(1U << bit);
2076 if (mrk & markType) {
2077 const QIcon markIcon = m_doc->markIcon(markType);
2078
2079 if (!markIcon.isNull() && h > 0 && m_iconAreaWidth > 0) {
2080 const int s = qMin(a: m_iconAreaWidth, b: static_cast<int>(h)) - 2;
2081
2082 // center the mark pixmap
2083 const int x_px = qMax(a: m_iconAreaWidth - s, b: 0) / 2;
2084 const int y_px = qMax(a: static_cast<int>(h) - s, b: 0) / 2;
2085
2086 markIcon.paint(painter: &p, x: lnX + x_px, y: y + y_px, w: s, h: s);
2087 }
2088 }
2089 }
2090 }
2091
2092 lnX += m_iconAreaWidth;
2093 if (m_updatePositionToArea) {
2094 m_positionToArea.push_back(x: AreaPosition(lnX, IconBorder));
2095 }
2096 }
2097
2098 // annotation information
2099 if (m_annotationBorderOn) {
2100 // Draw a border line between annotations and the line numbers
2101 p.setPen(lineNumberColor);
2102 p.setBrush(lineNumberColor);
2103
2104 const qreal borderX = lnX + m_annotationAreaWidth + 0.5;
2105 p.drawLine(p1: QPointF(borderX, y + 0.5), p2: QPointF(borderX, y + h - 0.5));
2106
2107 if (model) {
2108 KTextEditor::StyleOptionAnnotationItem styleOption;
2109 initStyleOption(styleOption: &styleOption);
2110 styleOption.rect.setRect(ax: lnX, ay: y, aw: m_annotationAreaWidth, ah: h);
2111 annotationGroupPositionState.nextLine(styleOption, z, realLine);
2112
2113 m_annotationItemDelegate->paint(painter: &p, option: styleOption, model, line: realLine);
2114 }
2115
2116 lnX += m_annotationAreaWidth + m_separatorWidth;
2117 if (m_updatePositionToArea) {
2118 m_positionToArea.push_back(x: AreaPosition(lnX, AnnotationBorder));
2119 }
2120 }
2121
2122 // line number
2123 if (m_lineNumbersOn || m_dynWrapIndicatorsOn) {
2124 QColor usedLineNumberColor;
2125 const int distanceToCurrent = abs(x: realLine - static_cast<int>(currentLine));
2126 if (distanceToCurrent == 0) {
2127 usedLineNumberColor = m_view->rendererConfig()->currentLineNumberColor();
2128 } else {
2129 usedLineNumberColor = lineNumberColor;
2130 }
2131 p.setPen(usedLineNumberColor);
2132 p.setBrush(usedLineNumberColor);
2133
2134 if (lineLayout.startCol() == 0) {
2135 if (m_relLineNumbersOn) {
2136 if (distanceToCurrent == 0) {
2137 p.drawText(x: lnX + m_maxCharWidth / 2,
2138 y,
2139 w: m_lineNumberAreaWidth - m_maxCharWidth,
2140 h,
2141 flags: Qt::TextDontClip | Qt::AlignLeft | Qt::AlignVCenter,
2142 str: QString::number(realLine + 1));
2143 } else {
2144 p.drawText(x: lnX + m_maxCharWidth / 2,
2145 y,
2146 w: m_lineNumberAreaWidth - m_maxCharWidth,
2147 h,
2148 flags: Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter,
2149 str: QString::number(distanceToCurrent));
2150 }
2151 if (m_updateRelLineNumbers) {
2152 m_updateRelLineNumbers = false;
2153 update();
2154 }
2155 } else if (m_lineNumbersOn) {
2156 p.drawText(x: lnX + m_maxCharWidth / 2,
2157 y,
2158 w: m_lineNumberAreaWidth - m_maxCharWidth,
2159 h,
2160 flags: Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter,
2161 str: QString::number(realLine + 1));
2162 }
2163 } else if (m_dynWrapIndicatorsOn) {
2164 p.drawText(x: lnX + m_maxCharWidth / 2,
2165 y,
2166 w: m_lineNumberAreaWidth - m_maxCharWidth,
2167 h,
2168 flags: Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter,
2169 str: m_dynWrapIndicatorChar);
2170 }
2171
2172 lnX += m_lineNumberAreaWidth + m_separatorWidth;
2173 if (m_updatePositionToArea) {
2174 m_positionToArea.push_back(x: AreaPosition(lnX, LineNumbers));
2175 }
2176 }
2177
2178 // modified line system
2179 if (m_view->config()->lineModification() && !m_doc->url().isEmpty()) {
2180 const auto tl = m_doc->plainKateTextLine(i: realLine);
2181 if (tl.markedAsModified()) {
2182 p.fillRect(x: lnX, y, w: m_modAreaWidth, h, b: m_view->rendererConfig()->modifiedLineColor());
2183 } else if (tl.markedAsSavedOnDisk()) {
2184 p.fillRect(x: lnX, y, w: m_modAreaWidth, h, b: m_view->rendererConfig()->savedLineColor());
2185 }
2186
2187 lnX += m_modAreaWidth; // No m_separatorWidth
2188 if (m_updatePositionToArea) {
2189 m_positionToArea.push_back(x: AreaPosition(lnX, None));
2190 }
2191 }
2192
2193 // folding markers
2194 if (m_foldingMarkersOn) {
2195 const QColor foldingColor(m_view->rendererConfig()->foldingColor());
2196 // possible additional folding highlighting
2197 if (m_foldingRange && m_foldingRange->overlapsLine(line: realLine)) {
2198 p.fillRect(x: lnX, y, w: m_foldingAreaWidth, h, b: foldingColor);
2199 }
2200
2201 if (lineLayout.startCol() == 0) {
2202 QList<QPair<qint64, Kate::TextFolding::FoldingRangeFlags>> startingRanges = m_view->textFolding().foldingRangesStartingOnLine(line: realLine);
2203 bool anyFolded = false;
2204 for (int i = 0; i < startingRanges.size(); ++i) {
2205 if (startingRanges[i].second & Kate::TextFolding::Folded) {
2206 anyFolded = true;
2207 }
2208 }
2209 if (!m_view->config()->showFoldingOnHoverOnly() || m_mouseOver) {
2210 if (!startingRanges.isEmpty() || m_doc->buffer().isFoldingStartingOnLine(startLine: realLine).first) {
2211 if (anyFolded) {
2212 paintTriangle(painter&: p, c: foldingColor, xOffset: lnX, yOffset: y, width: m_foldingAreaWidth, height: h, open: false);
2213 } else {
2214 // Don't try to use currentLineNumberColor, the folded icon gets also not highligted
2215 paintTriangle(painter&: p, c: lineNumberColor, xOffset: lnX, yOffset: y, width: m_foldingAreaWidth, height: h, open: true);
2216 }
2217 }
2218 }
2219 }
2220
2221 lnX += m_foldingAreaWidth;
2222 if (m_updatePositionToArea) {
2223 m_positionToArea.push_back(x: AreaPosition(lnX, FoldingMarkers));
2224 }
2225 }
2226 }
2227
2228 // Overpaint again the end to simulate some margin to the edit area,
2229 // so that the text not looks like stuck to the border
2230 // we do this AFTER all other painting to ensure this leaves no artifacts
2231 // we kill 2 separator widths as we will below paint a line over this
2232 // otherwise this has some minimal overlap and looks ugly e.g. for scaled rendering
2233 p.fillRect(x: w - 2 * m_separatorWidth, y, w, h, b: backgroundColor);
2234
2235 // overpaint again with selection or current line highlighting if necessary
2236 if (realLine >= 0 && m_view->selection() && !m_view->blockSelection() && m_view->selectionRange().start() < lineLayout.start()
2237 && m_view->selectionRange().end() >= lineLayout.start()) {
2238 // selection overpaint to signal the end of the previous line is included in the selection
2239 p.fillRect(x: w - 2 * m_separatorWidth, y, w, h, b: m_view->rendererConfig()->selectionColor());
2240 } else if (isCurrentLine) {
2241 // normal current line overpaint
2242 p.fillRect(x: w - 2 * m_separatorWidth, y, w, h, b: currentLineHighlight);
2243 }
2244
2245 // add separator line if needed
2246 // we do this AFTER all other painting to ensure this leaves no artifacts
2247 p.setPen(m_view->rendererConfig()->separatorColor());
2248 p.setBrush(m_view->rendererConfig()->separatorColor());
2249 p.drawLine(x1: w - 2 * m_separatorWidth, y1: y, x2: w - 2 * m_separatorWidth, y2: y + h);
2250
2251 // we might need to trigger geometry updates
2252 if ((realLine >= 0) && m_updatePositionToArea) {
2253 m_updatePositionToArea = false;
2254 // Don't forget our "text-stuck-to-border" protector + border line
2255 lnX += 2 * m_separatorWidth;
2256 m_positionToArea.push_back(x: AreaPosition(lnX, None));
2257
2258 // Now that we know our needed space, ensure we are painted properly
2259 // we still keep painting to not have strange flicker
2260 QTimer::singleShot(interval: 0, receiver: this, slot: &KateIconBorder::delayedUpdateOfSizeWithRepaint);
2261 }
2262 }
2263}
2264
2265KateIconBorder::BorderArea KateIconBorder::positionToArea(const QPoint &p) const
2266{
2267 auto it = std::find_if(first: m_positionToArea.cbegin(), last: m_positionToArea.cend(), pred: [p](const AreaPosition &ap) {
2268 return p.x() <= ap.first;
2269 });
2270 if (it != m_positionToArea.cend()) {
2271 return it->second;
2272 }
2273 return None;
2274}
2275
2276void KateIconBorder::mousePressEvent(QMouseEvent *e)
2277{
2278 const KateTextLayout &t = m_viewInternal->yToKateTextLayout(y: e->position().y());
2279 if (t.isValid()) {
2280 m_lastClickedLine = t.line();
2281 const auto area = positionToArea(p: e->pos());
2282 // IconBorder and AnnotationBorder have their own behavior; don't forward to view
2283 if (area != IconBorder && area != AnnotationBorder) {
2284 const auto pos = QPoint(0, e->position().y());
2285 if (area == LineNumbers && e->button() == Qt::LeftButton && !(e->modifiers() & Qt::ShiftModifier)) {
2286 // setup view so the following mousePressEvent will select the line
2287 m_viewInternal->beginSelectLine(pos);
2288 }
2289 QMouseEvent forward(QEvent::MouseButtonPress, pos, m_viewInternal->mapToGlobal(pos), e->button(), e->buttons(), e->modifiers());
2290 m_viewInternal->mousePressEvent(&forward);
2291 }
2292 return e->accept();
2293 }
2294
2295 QWidget::mousePressEvent(event: e);
2296}
2297
2298void KateIconBorder::highlightFoldingDelayed(int line)
2299{
2300 if ((line == m_currentLine) || (line >= m_doc->buffer().lines())) {
2301 return;
2302 }
2303
2304 m_currentLine = line;
2305
2306 if (m_foldingRange) {
2307 // We are for a while in the folding area, no need for delay
2308 highlightFolding();
2309
2310 } else if (!m_antiFlickerTimer.isActive()) {
2311 m_antiFlickerTimer.start();
2312 }
2313}
2314
2315void KateIconBorder::highlightFolding()
2316{
2317 // compute to which folding range we belong
2318 // FIXME: optimize + perhaps have some better threshold or use timers!
2319 KTextEditor::Range newRange = KTextEditor::Range::invalid();
2320 for (int line = m_currentLine; line >= qMax(a: 0, b: m_currentLine - 1024); --line) {
2321 // try if we have folding range from that line, should be fast per call
2322 KTextEditor::Range foldingRange = m_doc->buffer().computeFoldingRangeForStartLine(startLine: line);
2323 if (!foldingRange.isValid()) {
2324 continue;
2325 }
2326
2327 // does the range reach us?
2328 if (foldingRange.overlapsLine(line: m_currentLine)) {
2329 newRange = foldingRange;
2330 break;
2331 }
2332 }
2333
2334 if (newRange.isValid() && m_foldingRange && *m_foldingRange == newRange) {
2335 // new range equals the old one, nothing to do.
2336 return;
2337 }
2338
2339 // the ranges differ, delete the old, if it exists
2340 delete m_foldingRange;
2341 m_foldingRange = nullptr;
2342 // New range, new preview!
2343 delete m_foldingPreview;
2344
2345 bool showPreview = false;
2346
2347 if (newRange.isValid()) {
2348 // When next line is not visible we have a folded range, only then we want a preview!
2349 showPreview = !m_view->textFolding().isLineVisible(line: newRange.start().line() + 1);
2350
2351 // qCDebug(LOG_KTE) << "new folding hl-range:" << newRange;
2352 m_foldingRange = m_doc->newMovingRange(range: newRange, insertBehaviors: KTextEditor::MovingRange::ExpandRight);
2353 KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute());
2354
2355 // create highlighting color
2356 // we avoid alpha as overpainting leads to ugly lines (https://bugreports.qt.io/browse/QTBUG-66036)
2357 attr->setBackground(QBrush(m_view->rendererConfig()->foldingColor()));
2358
2359 m_foldingRange->setView(m_view);
2360 // use z depth defined in moving ranges interface
2361 m_foldingRange->setZDepth(-100.0);
2362 m_foldingRange->setAttribute(attr);
2363 }
2364
2365 // show text preview, if a folded region starts here...
2366 // ...but only when main window is active (#392396)
2367 const bool isWindowActive = !window() || window()->isActiveWindow();
2368 if (showPreview && m_view->config()->foldingPreview() && isWindowActive) {
2369 m_foldingPreview = new KateTextPreview(m_view, this);
2370 m_foldingPreview->setAttribute(Qt::WA_ShowWithoutActivating);
2371 m_foldingPreview->setFrameStyle(QFrame::StyledPanel);
2372
2373 // Calc how many lines can be displayed in the popup
2374 const int lineHeight = m_view->renderer()->lineHeight();
2375 const int foldingStartLine = m_foldingRange->start().line();
2376 // FIXME Is there really no easier way to find lineInDisplay?
2377 const QPoint pos = m_viewInternal->mapFrom(m_view, m_view->cursorToCoordinate(cursor: KTextEditor::Cursor(foldingStartLine, 0)));
2378 const int lineInDisplay = pos.y() / lineHeight;
2379 // Allow slightly overpainting of the view bottom to proper cover all lines
2380 const int extra = (m_viewInternal->height() % lineHeight) > (lineHeight * 0.6) ? 1 : 0;
2381 const int lineCount = qMin(a: m_foldingRange->numberOfLines() + 1, b: m_viewInternal->linesDisplayed() - lineInDisplay + extra);
2382
2383 m_foldingPreview->resize(w: m_viewInternal->width(), h: lineCount * lineHeight + 2 * m_foldingPreview->frameWidth());
2384 const int xGlobal = mapToGlobal(QPoint(width(), 0)).x();
2385 const int yGlobal = m_view->mapToGlobal(m_view->cursorToCoordinate(cursor: KTextEditor::Cursor(foldingStartLine, 0))).y();
2386 m_foldingPreview->move(QPoint(xGlobal, yGlobal) - m_foldingPreview->contentsRect().topLeft());
2387 m_foldingPreview->setLine(foldingStartLine);
2388 m_foldingPreview->setCenterView(false);
2389 m_foldingPreview->setShowFoldedLines(true);
2390 m_foldingPreview->raise();
2391 m_foldingPreview->show();
2392 }
2393}
2394
2395void KateIconBorder::hideFolding()
2396{
2397 if (m_antiFlickerTimer.isActive()) {
2398 m_antiFlickerTimer.stop();
2399 }
2400
2401 m_currentLine = -1;
2402 delete m_foldingRange;
2403 m_foldingRange = nullptr;
2404
2405 delete m_foldingPreview;
2406}
2407
2408void KateIconBorder::enterEvent(QEnterEvent *event)
2409{
2410 m_mouseOver = true;
2411 if (m_view->config()->showFoldingOnHoverOnly())
2412 repaint();
2413 QWidget::enterEvent(event);
2414}
2415
2416void KateIconBorder::leaveEvent(QEvent *event)
2417{
2418 m_mouseOver = false;
2419 hideFolding();
2420 removeAnnotationHovering();
2421 if (m_view->config()->showFoldingOnHoverOnly())
2422 repaint();
2423
2424 QWidget::leaveEvent(event);
2425}
2426
2427void KateIconBorder::mouseMoveEvent(QMouseEvent *e)
2428{
2429 const KateTextLayout &t = m_viewInternal->yToKateTextLayout(y: e->position().y());
2430 if (!t.isValid()) {
2431 // Cleanup everything which may be shown
2432 removeAnnotationHovering();
2433 hideFolding();
2434
2435 } else {
2436 const BorderArea area = positionToArea(p: e->pos());
2437 if (area == FoldingMarkers) {
2438 highlightFoldingDelayed(line: t.line());
2439 } else {
2440 hideFolding();
2441 }
2442 if (area == AnnotationBorder) {
2443 KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel();
2444 if (model) {
2445 m_hoveredAnnotationGroupIdentifier = model->data(line: t.line(), role: (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole).toString();
2446 const QPoint viewRelativePos = m_view->mapFromGlobal(e->globalPosition()).toPoint();
2447 QHelpEvent helpEvent(QEvent::ToolTip, viewRelativePos, e->globalPosition().toPoint());
2448 KTextEditor::StyleOptionAnnotationItem styleOption;
2449 initStyleOption(styleOption: &styleOption);
2450 styleOption.rect = annotationLineRectInView(line: t.line());
2451 setStyleOptionLineData(styleOption: &styleOption, y: e->position().y(), realLine: t.line(), model, annotationGroupIdentifier: m_hoveredAnnotationGroupIdentifier);
2452 m_annotationItemDelegate->helpEvent(event: &helpEvent, view: m_view, option: styleOption, model, line: t.line());
2453
2454 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
2455 }
2456 } else {
2457 if (area == IconBorder) {
2458 m_doc->requestMarkTooltip(line: t.line(), position: e->globalPosition().toPoint());
2459 }
2460
2461 m_hoveredAnnotationGroupIdentifier.clear();
2462 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
2463 }
2464 if (area != IconBorder) {
2465 QPoint p = m_viewInternal->mapFromGlobal(e->globalPosition().toPoint());
2466 QMouseEvent forward(QEvent::MouseMove, p, m_viewInternal->mapToGlobal(p), e->button(), e->buttons(), e->modifiers());
2467 m_viewInternal->mouseMoveEvent(&forward);
2468 }
2469 }
2470
2471 QWidget::mouseMoveEvent(event: e);
2472}
2473
2474void KateIconBorder::mouseReleaseEvent(QMouseEvent *e)
2475{
2476 const int cursorOnLine = m_viewInternal->yToKateTextLayout(y: e->position().y()).line();
2477 if (cursorOnLine == m_lastClickedLine && cursorOnLine >= 0 && cursorOnLine <= m_doc->lastLine()) {
2478 const BorderArea area = positionToArea(p: e->pos());
2479 if (area == IconBorder) {
2480 if (e->button() == Qt::LeftButton) {
2481 if (!m_doc->handleMarkClick(line: cursorOnLine)) {
2482 KateViewConfig *config = m_view->config();
2483 const uint editBits = m_doc->editableMarks();
2484 // is the default or the only editable mark
2485 bool ctrlPressed = QGuiApplication::keyboardModifiers() == Qt::KeyboardModifier::ControlModifier;
2486 if (qPopulationCount(v: editBits) == 1 || ctrlPressed) {
2487 const uint singleMark = (qPopulationCount(v: editBits) > 1) ? (editBits & config->defaultMarkType()) : editBits;
2488 if (m_doc->mark(line: cursorOnLine) & singleMark) {
2489 m_doc->removeMark(line: cursorOnLine, markType: singleMark);
2490 } else {
2491 m_doc->addMark(line: cursorOnLine, markType: singleMark);
2492 }
2493 } else if (config->allowMarkMenu()) {
2494 showMarkMenu(line: cursorOnLine, pos: QCursor::pos());
2495 }
2496 }
2497 } else if (e->button() == Qt::RightButton) {
2498 showMarkMenu(line: cursorOnLine, pos: QCursor::pos());
2499 }
2500 }
2501
2502 if (area == FoldingMarkers) {
2503 // Prefer the highlighted range over the exact clicked line
2504 const int lineToToggle = m_foldingRange ? m_foldingRange->toRange().start().line() : cursorOnLine;
2505 if (e->button() == Qt::LeftButton) {
2506 m_view->toggleFoldingOfLine(line: lineToToggle);
2507 } else if (e->button() == Qt::RightButton) {
2508 m_view->toggleFoldingsInRange(line: lineToToggle);
2509 }
2510
2511 delete m_foldingPreview;
2512 }
2513
2514 if (area == AnnotationBorder) {
2515 const bool singleClick = style()->styleHint(stylehint: QStyle::SH_ItemView_ActivateItemOnSingleClick, opt: nullptr, widget: this);
2516 if (e->button() == Qt::LeftButton && singleClick) {
2517 Q_EMIT m_view->annotationActivated(view: m_view, line: cursorOnLine);
2518 }
2519 }
2520 }
2521
2522 const QPoint pos(0, e->position().y());
2523 QMouseEvent forward(QEvent::MouseButtonRelease, pos, m_viewInternal->mapToGlobal(pos), e->button(), e->buttons(), e->modifiers());
2524 m_viewInternal->mouseReleaseEvent(&forward);
2525}
2526
2527void KateIconBorder::mouseDoubleClickEvent(QMouseEvent *e)
2528{
2529 int cursorOnLine = m_viewInternal->yToKateTextLayout(y: e->position().y()).line();
2530
2531 if (cursorOnLine == m_lastClickedLine && cursorOnLine <= m_doc->lastLine()) {
2532 const BorderArea area = positionToArea(p: e->pos());
2533 const bool singleClick = style()->styleHint(stylehint: QStyle::SH_ItemView_ActivateItemOnSingleClick, opt: nullptr, widget: this);
2534 if (area == AnnotationBorder && !singleClick) {
2535 Q_EMIT m_view->annotationActivated(view: m_view, line: cursorOnLine);
2536 }
2537 }
2538 const QPoint pos(0, e->position().y());
2539 QMouseEvent forward(QEvent::MouseButtonDblClick, pos, m_viewInternal->mapToGlobal(pos), e->button(), e->buttons(), e->modifiers());
2540 m_viewInternal->mouseDoubleClickEvent(&forward);
2541}
2542
2543void KateIconBorder::contextMenuEvent(QContextMenuEvent *e)
2544{
2545 const BorderArea area = positionToArea(p: e->pos());
2546 const int cursorOnLine = m_viewInternal->yToKateTextLayout(y: e->y()).line();
2547
2548 if (area == AnnotationBorder) {
2549 showAnnotationMenu(line: cursorOnLine, pos: e->globalPos());
2550 return;
2551 }
2552
2553 QMenu menu(this);
2554
2555 KActionCollection *ac = m_view->actionCollection();
2556
2557 // NOTE Assumes cursor position was updated before the menu opens
2558 if (QAction *bookmarkToggle = ac->action(QStringLiteral("bookmarks_toggle"))) {
2559 menu.addAction(action: bookmarkToggle);
2560 }
2561 if (QAction *bookmarkClear = ac->action(QStringLiteral("bookmarks_clear"))) {
2562 menu.addAction(action: bookmarkClear);
2563 }
2564
2565 menu.addSeparator();
2566
2567 if (auto a = ac->action(QStringLiteral("edit_copy_file_location"))) {
2568 menu.addAction(action: a);
2569 }
2570
2571 menu.addSeparator();
2572
2573 if (QAction *toggleDynWrap = ac->action(QStringLiteral("view_dynamic_word_wrap"))) {
2574 menu.addAction(action: toggleDynWrap);
2575 }
2576 menu.addSeparator();
2577 if (QAction *toggleIconBar = ac->action(QStringLiteral("view_border"))) {
2578 menu.addAction(action: toggleIconBar);
2579 }
2580 if (QAction *toggleLineNumbers = ac->action(QStringLiteral("view_line_numbers"))) {
2581 menu.addAction(action: toggleLineNumbers);
2582 }
2583 if (QAction *toggleFoldingMarkers = ac->action(QStringLiteral("view_folding_markers"))) {
2584 menu.addAction(action: toggleFoldingMarkers);
2585 }
2586
2587 menu.exec(pos: e->globalPos());
2588}
2589
2590void KateIconBorder::wheelEvent(QWheelEvent *e)
2591{
2592 QCoreApplication::sendEvent(receiver: m_viewInternal, event: e);
2593}
2594
2595void KateIconBorder::showMarkMenu(uint line, const QPoint &pos)
2596{
2597 if (m_doc->handleMarkContextMenu(line, position: pos)) {
2598 return;
2599 }
2600
2601 if (!m_view->config()->allowMarkMenu()) {
2602 return;
2603 }
2604
2605 QMenu markMenu;
2606 QMenu selectDefaultMark;
2607 auto selectDefaultMarkActionGroup = new QActionGroup(&selectDefaultMark);
2608
2609 std::vector<int> vec(33);
2610 int i = 1;
2611
2612 for (uint bit = 0; bit < 32; bit++) {
2613 KTextEditor::Document::MarkTypes markType = (KTextEditor::Document::MarkTypes)(1U << bit);
2614 if (!(m_doc->editableMarks() & markType)) {
2615 continue;
2616 }
2617
2618 QAction *mA;
2619 QAction *dMA;
2620 const QIcon icon = m_doc->markIcon(markType);
2621 if (!m_doc->markDescription(markType).isEmpty()) {
2622 mA = markMenu.addAction(icon, text: m_doc->markDescription(markType));
2623 dMA = selectDefaultMark.addAction(icon, text: m_doc->markDescription(markType));
2624 } else {
2625 mA = markMenu.addAction(icon, i18n("Mark Type %1", bit + 1));
2626 dMA = selectDefaultMark.addAction(icon, i18n("Mark Type %1", bit + 1));
2627 }
2628 selectDefaultMarkActionGroup->addAction(a: dMA);
2629 mA->setData(i);
2630 mA->setCheckable(true);
2631 dMA->setData(i + 100);
2632 dMA->setCheckable(true);
2633 if (m_doc->mark(line) & markType) {
2634 mA->setChecked(true);
2635 }
2636
2637 if (markType & KateViewConfig::global()->defaultMarkType()) {
2638 dMA->setChecked(true);
2639 }
2640
2641 vec[i++] = markType;
2642 }
2643
2644 if (markMenu.actions().count() == 0) {
2645 return;
2646 }
2647
2648 if (markMenu.actions().count() > 1) {
2649 markMenu.addAction(i18n("Set Default Mark Type"))->setMenu(&selectDefaultMark);
2650 }
2651
2652 QAction *rA = markMenu.exec(pos);
2653 if (!rA) {
2654 return;
2655 }
2656 int result = rA->data().toInt();
2657 if (result > 100) {
2658 KateViewConfig::global()->setValue(key: KateViewConfig::DefaultMarkType, value: vec[result - 100]);
2659 } else {
2660 KTextEditor::Document::MarkTypes markType = (KTextEditor::Document::MarkTypes)vec[result];
2661 if (m_doc->mark(line) & markType) {
2662 m_doc->removeMark(line, markType);
2663 } else {
2664 m_doc->addMark(line, markType);
2665 }
2666 }
2667}
2668
2669KTextEditor::AbstractAnnotationItemDelegate *KateIconBorder::annotationItemDelegate() const
2670{
2671 return m_annotationItemDelegate;
2672}
2673
2674void KateIconBorder::setAnnotationItemDelegate(KTextEditor::AbstractAnnotationItemDelegate *delegate)
2675{
2676 if (delegate == m_annotationItemDelegate) {
2677 return;
2678 }
2679
2680 // reset to default, but already on that?
2681 if (!delegate && m_isDefaultAnnotationItemDelegate) {
2682 // nothing to do
2683 return;
2684 }
2685
2686 // make sure the tooltip is hidden
2687 if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) {
2688 m_hoveredAnnotationGroupIdentifier.clear();
2689 hideAnnotationTooltip();
2690 }
2691
2692 disconnect(sender: m_annotationItemDelegate, signal: &AbstractAnnotationItemDelegate::sizeHintChanged, receiver: this, slot: &KateIconBorder::updateAnnotationBorderWidth);
2693 if (!m_isDefaultAnnotationItemDelegate) {
2694 disconnect(sender: m_annotationItemDelegate, signal: &QObject::destroyed, receiver: this, slot: &KateIconBorder::handleDestroyedAnnotationItemDelegate);
2695 }
2696
2697 if (!delegate) {
2698 // reset to a default delegate
2699 m_annotationItemDelegate = new KateAnnotationItemDelegate(this);
2700 m_isDefaultAnnotationItemDelegate = true;
2701 } else {
2702 // drop any default delegate
2703 if (m_isDefaultAnnotationItemDelegate) {
2704 delete m_annotationItemDelegate;
2705 m_isDefaultAnnotationItemDelegate = false;
2706 }
2707
2708 m_annotationItemDelegate = delegate;
2709 // catch delegate being destroyed
2710 connect(sender: delegate, signal: &QObject::destroyed, context: this, slot: &KateIconBorder::handleDestroyedAnnotationItemDelegate);
2711 }
2712
2713 connect(sender: m_annotationItemDelegate, signal: &AbstractAnnotationItemDelegate::sizeHintChanged, context: this, slot: &KateIconBorder::updateAnnotationBorderWidth);
2714
2715 if (m_annotationBorderOn) {
2716 QTimer::singleShot(interval: 0, receiver: this, slot: &KateIconBorder::delayedUpdateOfSizeWithRepaint);
2717 }
2718}
2719
2720void KateIconBorder::handleDestroyedAnnotationItemDelegate()
2721{
2722 setAnnotationItemDelegate(nullptr);
2723}
2724
2725void KateIconBorder::delayedUpdateOfSizeWithRepaint()
2726{
2727 // ensure we update size + repaint at once to avoid flicker, see bug 435361
2728 setUpdatesEnabled(false);
2729 updateGeometry();
2730 repaint();
2731 setUpdatesEnabled(true);
2732}
2733
2734void KateIconBorder::initStyleOption(KTextEditor::StyleOptionAnnotationItem *styleOption) const
2735{
2736 styleOption->initFrom(w: this);
2737 styleOption->view = m_view;
2738 styleOption->decorationSize = QSize(m_iconAreaWidth, m_iconAreaWidth);
2739 styleOption->contentFontMetrics = m_view->renderer()->currentFontMetrics();
2740}
2741
2742void KateIconBorder::setStyleOptionLineData(KTextEditor::StyleOptionAnnotationItem *styleOption,
2743 int y,
2744 int realLine,
2745 const KTextEditor::AnnotationModel *model,
2746 const QString &annotationGroupIdentifier) const
2747{
2748 // calculate rendered displayed line
2749 const uint h = m_view->renderer()->lineHeight();
2750 const uint z = (y / h);
2751
2752 KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, annotationGroupIdentifier, z, true);
2753 annotationGroupPositionState.nextLine(styleOption&: *styleOption, z, realLine);
2754}
2755
2756QRect KateIconBorder::annotationLineRectInView(int line) const
2757{
2758 int x = 0;
2759 if (m_iconBorderOn) {
2760 x += m_iconAreaWidth + 2;
2761 }
2762 const int y = m_view->m_viewInternal->lineToY(viewLine: line);
2763
2764 return QRect(x, y, m_annotationAreaWidth, m_view->renderer()->lineHeight());
2765}
2766
2767void KateIconBorder::updateAnnotationLine(int line)
2768{
2769 // TODO: why has the default value been 8, where is that magic number from?
2770 int width = 8;
2771 KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel();
2772
2773 if (model) {
2774 KTextEditor::StyleOptionAnnotationItem styleOption;
2775 initStyleOption(styleOption: &styleOption);
2776 width = m_annotationItemDelegate->sizeHint(option: styleOption, model, line).width();
2777 }
2778
2779 if (width > m_annotationAreaWidth) {
2780 m_annotationAreaWidth = width;
2781 m_updatePositionToArea = true;
2782
2783 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
2784 }
2785}
2786
2787void KateIconBorder::showAnnotationMenu(int line, const QPoint &pos)
2788{
2789 QMenu menu;
2790 QAction a(i18n("Disable Annotation Bar"), &menu);
2791 a.setIcon(QIcon::fromTheme(QStringLiteral("dialog-close")));
2792 menu.addAction(action: &a);
2793 Q_EMIT m_view->annotationContextMenuAboutToShow(view: m_view, menu: &menu, line);
2794 if (menu.exec(pos) == &a) {
2795 m_view->setAnnotationBorderVisible(false);
2796 }
2797}
2798
2799void KateIconBorder::hideAnnotationTooltip()
2800{
2801 m_annotationItemDelegate->hideTooltip(view: m_view);
2802}
2803
2804void KateIconBorder::updateAnnotationBorderWidth()
2805{
2806 calcAnnotationBorderWidth();
2807
2808 m_updatePositionToArea = true;
2809
2810 QTimer::singleShot(msec: 0, receiver: this, SLOT(update()));
2811}
2812
2813void KateIconBorder::calcAnnotationBorderWidth()
2814{
2815 // TODO: another magic number, not matching the one in updateAnnotationLine()
2816 m_annotationAreaWidth = 6;
2817 KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel();
2818
2819 if (model) {
2820 KTextEditor::StyleOptionAnnotationItem styleOption;
2821 initStyleOption(styleOption: &styleOption);
2822
2823 const int lineCount = m_view->doc()->lines();
2824 if (lineCount > 0) {
2825 const int checkedLineCount = m_hasUniformAnnotationItemSizes ? 1 : lineCount;
2826 for (int i = 0; i < checkedLineCount; ++i) {
2827 const int curwidth = m_annotationItemDelegate->sizeHint(option: styleOption, model, line: i).width();
2828 if (curwidth > m_annotationAreaWidth) {
2829 m_annotationAreaWidth = curwidth;
2830 }
2831 }
2832 }
2833 }
2834}
2835
2836void KateIconBorder::annotationModelChanged(KTextEditor::AnnotationModel *oldmodel, KTextEditor::AnnotationModel *newmodel)
2837{
2838 if (oldmodel) {
2839 oldmodel->disconnect(receiver: this);
2840 }
2841 if (newmodel) {
2842 connect(sender: newmodel, signal: &KTextEditor::AnnotationModel::reset, context: this, slot: &KateIconBorder::updateAnnotationBorderWidth);
2843 connect(sender: newmodel, signal: &KTextEditor::AnnotationModel::lineChanged, context: this, slot: &KateIconBorder::updateAnnotationLine);
2844 }
2845 updateAnnotationBorderWidth();
2846}
2847
2848void KateIconBorder::displayRangeChanged()
2849{
2850 hideFolding();
2851 removeAnnotationHovering();
2852}
2853
2854// END KateIconBorder
2855
2856// BEGIN KateViewEncodingAction
2857// According to https://www.iana.org/assignments/ianacharset-mib/ianacharset-mib
2858// the default/unknown mib value is 2.
2859#define MIB_DEFAULT 2
2860
2861bool lessThanAction(KSelectAction *a, KSelectAction *b)
2862{
2863 return a->text() < b->text();
2864}
2865
2866void KateViewEncodingAction::init()
2867{
2868 QList<KSelectAction *> actions;
2869
2870 setToolBarMode(MenuMode);
2871
2872 int i;
2873 const auto encodingsByScript = KCharsets::charsets()->encodingsByScript();
2874 actions.reserve(asize: encodingsByScript.size());
2875 for (const QStringList &encodingsForScript : encodingsByScript) {
2876 KSelectAction *tmp = new KSelectAction(encodingsForScript.at(i: 0), this);
2877
2878 for (i = 1; i < encodingsForScript.size(); ++i) {
2879 tmp->addAction(text: encodingsForScript.at(i));
2880 }
2881 connect(sender: tmp, signal: &KSelectAction::actionTriggered, context: this, slot: [this](QAction *action) {
2882 subActionTriggered(action);
2883 });
2884 // tmp->setCheckable(true);
2885 actions << tmp;
2886 }
2887 std::sort(first: actions.begin(), last: actions.end(), comp: lessThanAction);
2888 for (KSelectAction *action : std::as_const(t&: actions)) {
2889 addAction(action);
2890 }
2891}
2892
2893void KateViewEncodingAction::subActionTriggered(QAction *action)
2894{
2895 if (currentSubAction == action) {
2896 return;
2897 }
2898 currentSubAction = action;
2899 Q_EMIT textTriggered(text: action->text());
2900}
2901
2902KateViewEncodingAction::KateViewEncodingAction(KTextEditor::DocumentPrivate *_doc,
2903 KTextEditor::ViewPrivate *_view,
2904 const QString &text,
2905 QObject *parent,
2906 bool saveAsMode)
2907 : KSelectAction(text, parent)
2908 , doc(_doc)
2909 , view(_view)
2910 , m_saveAsMode(saveAsMode)
2911{
2912 init();
2913
2914 connect(sender: menu(), signal: &QMenu::aboutToShow, context: this, slot: &KateViewEncodingAction::slotAboutToShow);
2915 connect(sender: this, signal: &KSelectAction::textTriggered, context: this, slot: &KateViewEncodingAction::setEncoding);
2916}
2917
2918void KateViewEncodingAction::slotAboutToShow()
2919{
2920 setCurrentCodec(doc->config()->encoding());
2921}
2922
2923void KateViewEncodingAction::setEncoding(const QString &e)
2924{
2925 // in save as mode => trigger saveAs
2926 if (m_saveAsMode) {
2927 doc->documentSaveAsWithEncoding(encoding: e);
2928 return;
2929 }
2930
2931 // else switch encoding
2932 doc->userSetEncodingForNextReload();
2933 doc->setEncoding(e);
2934 view->reloadFile();
2935}
2936
2937bool KateViewEncodingAction::setCurrentCodec(const QString &codec)
2938{
2939 disconnect(sender: this, signal: &KSelectAction::textTriggered, receiver: this, slot: &KateViewEncodingAction::setEncoding);
2940
2941 int i;
2942 int j;
2943 for (i = 0; i < actions().size(); ++i) {
2944 if (actions().at(i)->menu()) {
2945 for (j = 0; j < actions().at(i)->menu()->actions().size(); ++j) {
2946 if (!j && !actions().at(i)->menu()->actions().at(i: j)->data().isNull()) {
2947 continue;
2948 }
2949 if (actions().at(i)->menu()->actions().at(i: j)->isSeparator()) {
2950 continue;
2951 }
2952
2953 if (codec == actions().at(i)->menu()->actions().at(i: j)->text()) {
2954 currentSubAction = actions().at(i)->menu()->actions().at(i: j);
2955 currentSubAction->setChecked(true);
2956 } else {
2957 actions().at(i)->menu()->actions().at(i: j)->setChecked(false);
2958 }
2959 }
2960 }
2961 }
2962
2963 connect(sender: this, signal: &KSelectAction::textTriggered, context: this, slot: &KateViewEncodingAction::setEncoding);
2964 return true;
2965}
2966
2967// END KateViewEncodingAction
2968
2969// BEGIN KateViewBar related classes
2970
2971KateViewBarWidget::KateViewBarWidget(bool addCloseButton, QWidget *parent)
2972 : QWidget(parent)
2973 , m_viewBar(nullptr)
2974{
2975 QHBoxLayout *layout = new QHBoxLayout(this);
2976
2977 // NOTE: Here be cosmetics.
2978 layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
2979
2980 // widget to be used as parent for the real content
2981 m_centralWidget = new QWidget(this);
2982 layout->addWidget(m_centralWidget);
2983 setFocusProxy(m_centralWidget);
2984
2985 // hide button
2986 if (addCloseButton) {
2987 m_closeButton = new QToolButton(this);
2988 m_closeButton->setAutoRaise(true);
2989 m_closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close")));
2990 connect(sender: m_closeButton, signal: &QToolButton::clicked, context: this, slot: &KateViewBarWidget::hideMe);
2991 layout->addWidget(m_closeButton);
2992 layout->setAlignment(w: m_closeButton, alignment: Qt::AlignCenter | Qt::AlignVCenter);
2993 }
2994}
2995
2996KateViewBar::KateViewBar(bool external, QWidget *parent, KTextEditor::ViewPrivate *view)
2997 : QWidget(parent)
2998 , m_external(external)
2999 , m_view(view)
3000 , m_permanentBarWidget(nullptr)
3001
3002{
3003 m_layout = new QVBoxLayout(this);
3004 m_stack = new QStackedWidget(this);
3005 m_layout->addWidget(m_stack);
3006 m_layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
3007
3008 m_stack->hide();
3009 hide();
3010}
3011
3012void KateViewBar::addBarWidget(KateViewBarWidget *newBarWidget)
3013{
3014 // just ignore additional adds for already existing widgets
3015 if (hasBarWidget(barWidget: newBarWidget)) {
3016 return;
3017 }
3018
3019 // add new widget, invisible...
3020 newBarWidget->hide();
3021 m_stack->addWidget(w: newBarWidget);
3022 newBarWidget->setAssociatedViewBar(this);
3023 connect(sender: newBarWidget, signal: &KateViewBarWidget::hideMe, context: this, slot: &KateViewBar::hideCurrentBarWidget);
3024}
3025
3026void KateViewBar::removeBarWidget(KateViewBarWidget *barWidget)
3027{
3028 // remove only if there
3029 if (!hasBarWidget(barWidget)) {
3030 return;
3031 }
3032
3033 m_stack->removeWidget(w: barWidget);
3034 barWidget->setAssociatedViewBar(nullptr);
3035 barWidget->hide();
3036 disconnect(sender: barWidget, signal: nullptr, receiver: this, member: nullptr);
3037}
3038
3039void KateViewBar::addPermanentBarWidget(KateViewBarWidget *barWidget)
3040{
3041 Q_ASSERT(barWidget);
3042 Q_ASSERT(!m_permanentBarWidget);
3043
3044 m_layout->addWidget(barWidget);
3045 m_permanentBarWidget = barWidget;
3046 m_permanentBarWidget->show();
3047
3048 setViewBarVisible(true);
3049}
3050
3051void KateViewBar::removePermanentBarWidget(KateViewBarWidget *barWidget)
3052{
3053 Q_ASSERT(m_permanentBarWidget == barWidget);
3054 Q_UNUSED(barWidget);
3055
3056 m_permanentBarWidget->hide();
3057 m_layout->removeWidget(w: m_permanentBarWidget);
3058 m_permanentBarWidget = nullptr;
3059
3060 if (!barWidgetVisible()) {
3061 setViewBarVisible(false);
3062 }
3063}
3064
3065void KateViewBar::showBarWidget(KateViewBarWidget *barWidget)
3066{
3067 Q_ASSERT(barWidget != nullptr);
3068
3069 if (barWidget != qobject_cast<KateViewBarWidget *>(object: m_stack->currentWidget())) {
3070 hideCurrentBarWidget();
3071 }
3072
3073 // raise correct widget
3074 m_stack->addWidget(w: barWidget);
3075 m_stack->setCurrentWidget(barWidget);
3076 barWidget->show();
3077 barWidget->setFocus(Qt::ShortcutFocusReason);
3078 m_stack->show();
3079 setViewBarVisible(true);
3080}
3081
3082bool KateViewBar::hasBarWidget(KateViewBarWidget *barWidget) const
3083{
3084 return m_stack->indexOf(barWidget) != -1;
3085}
3086
3087void KateViewBar::hideCurrentBarWidget()
3088{
3089 KateViewBarWidget *current = qobject_cast<KateViewBarWidget *>(object: m_stack->currentWidget());
3090 if (current) {
3091 m_stack->removeWidget(w: current);
3092 current->closed();
3093 }
3094
3095 // hide the bar
3096 m_stack->hide();
3097 if (!m_permanentBarWidget) {
3098 setViewBarVisible(false);
3099 }
3100
3101 m_view->setFocus();
3102}
3103
3104void KateViewBar::setViewBarVisible(bool visible)
3105{
3106 if (m_external) {
3107 if (visible) {
3108 m_view->mainWindow()->showViewBar(view: m_view);
3109 } else {
3110 m_view->mainWindow()->hideViewBar(view: m_view);
3111 }
3112 } else {
3113 setVisible(visible);
3114 }
3115}
3116
3117bool KateViewBar::barWidgetVisible() const
3118{
3119 KateViewBarWidget *current = qobject_cast<KateViewBarWidget *>(object: m_stack->currentWidget());
3120 return current && current->isVisible();
3121}
3122
3123void KateViewBar::keyPressEvent(QKeyEvent *event)
3124{
3125 if (event->key() == Qt::Key_Escape) {
3126 hideCurrentBarWidget();
3127 return;
3128 }
3129 QWidget::keyPressEvent(event);
3130}
3131
3132void KateViewBar::hideEvent(QHideEvent *event)
3133{
3134 Q_UNUSED(event);
3135 // if (!event->spontaneous())
3136 // m_view->setFocus();
3137}
3138
3139// END KateViewBar related classes
3140
3141// BEGIN SCHEMA ACTION -- the 'View->Color theme' menu action
3142void KateViewSchemaAction::init()
3143{
3144 m_group = nullptr;
3145 m_view = nullptr;
3146 last = 0;
3147
3148 connect(sender: menu(), signal: &QMenu::aboutToShow, context: this, slot: &KateViewSchemaAction::slotAboutToShow);
3149}
3150
3151void KateViewSchemaAction::updateMenu(KTextEditor::ViewPrivate *view)
3152{
3153 m_view = view;
3154}
3155
3156void KateViewSchemaAction::slotAboutToShow()
3157{
3158 KTextEditor::ViewPrivate *view = m_view;
3159
3160 const auto themes = KateHlManager::self()->sortedThemes();
3161
3162 if (!m_group) {
3163 m_group = new QActionGroup(menu());
3164 m_group->setExclusive(true);
3165 }
3166
3167 for (int z = 0; z < themes.count(); z++) {
3168 QString hlName = themes[z].translatedName();
3169
3170 if (!names.contains(str: hlName)) {
3171 names << hlName;
3172 QAction *a = menu()->addAction(text: hlName, args: this, args: &KateViewSchemaAction::setSchema);
3173 a->setData(themes[z].name());
3174 a->setCheckable(true);
3175 a->setActionGroup(m_group);
3176 }
3177 }
3178
3179 if (!view) {
3180 return;
3181 }
3182
3183 QString id = view->rendererConfig()->schema();
3184 const auto menuActions = menu()->actions();
3185 for (QAction *a : menuActions) {
3186 a->setChecked(a->data().toString() == id);
3187 }
3188}
3189
3190void KateViewSchemaAction::setSchema()
3191{
3192 QAction *action = qobject_cast<QAction *>(object: sender());
3193
3194 if (!action) {
3195 return;
3196 }
3197 QString mode = action->data().toString();
3198
3199 KTextEditor::ViewPrivate *view = m_view;
3200
3201 if (view) {
3202 view->rendererConfig()->setSchema(mode);
3203 }
3204}
3205// END SCHEMA ACTION
3206
3207#include "moc_kateviewhelpers.cpp"
3208

source code of ktexteditor/src/view/kateviewhelpers.cpp