1 /*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2002 John Firebaugh <jfirebaugh@kde.org>
4 SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
5 SPDX-FileCopyrightText: 2002, 2003 Christoph Cullmann <cullmann@kde.org>
6 SPDX-FileCopyrightText: 2002-2007 Hamish Rodda <rodda@kde.org>
7 SPDX-FileCopyrightText: 2003 Anakim Border <aborder@sources.sourceforge.net>
8 SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
9 SPDX-FileCopyrightText: 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
10 SPDX-FileCopyrightText: 2008 Erlend Hamberg <ehamberg@gmail.com>
11
12 Based on KWriteView:
13 SPDX-FileCopyrightText: 1999 Jochen Wilhelmy <digisnap@cs.tu-berlin.de>
14
15 SPDX-License-Identifier: LGPL-2.0-only
16 */
17 #include "kateviewinternal.h"
18
19 #include "kateabstractinputmode.h"
20 #include "kateabstractinputmodefactory.h"
21 #include "katebuffer.h"
22 #include "katecompletionwidget.h"
23 #include "kateconfig.h"
24 #include "kateglobal.h"
25 #include "katehighlight.h"
26 #include "katelayoutcache.h"
27 #include "katemessagewidget.h"
28 #include "katepartdebug.h"
29 #include "katetextanimation.h"
30 #include "katetextpreview.h"
31 #include "kateview.h"
32 #include "kateviewaccessible.h"
33 #include "kateviewhelpers.h"
34 #include "spellcheck/spellingmenu.h"
35
36 #include <KCursor>
37 #include <ktexteditor/documentcursor.h>
38 #include <ktexteditor/inlinenoteprovider.h>
39 #include <ktexteditor/movingrange.h>
40 #include <ktexteditor/texthintinterface.h>
41
42 #include <QAccessible>
43 #include <QApplication>
44 #include <QClipboard>
45 #include <QKeyEvent>
46 #include <QLayout>
47 #include <QMimeData>
48 #include <QPainter>
49 #include <QPixmap>
50 #include <QScroller>
51 #include <QStyle>
52 #include <QToolTip>
53
54 static const bool debugPainting = false;
55
56 class ZoomEventFilter
57 {
58 public:
59 ZoomEventFilter() = default;
60
detectZoomingEvent(QWheelEvent * e,Qt::KeyboardModifiers modifier=Qt::ControlModifier)61 bool detectZoomingEvent(QWheelEvent *e, Qt::KeyboardModifiers modifier = Qt::ControlModifier)
62 {
63 Qt::KeyboardModifiers modState = e->modifiers();
64 if (modState == modifier) {
65 if (m_lastWheelEvent.isValid()) {
66 const qint64 deltaT = m_lastWheelEvent.elapsed();
67 // Pressing the specified modifier key within 200ms of the previous "unmodified"
68 // wheelevent is not allowed to toggle on text zooming
69 if (m_lastWheelEventUnmodified && deltaT < 200) {
70 m_ignoreZoom = true;
71 } else if (deltaT > 1000) {
72 // the protection is kept active for 1s after the last wheel event
73 // TODO: this value should be tuned, preferably by someone using
74 // Ctrl+Wheel zooming frequently.
75 m_ignoreZoom = false;
76 }
77 } else {
78 // we can't say anything and have to assume there's nothing
79 // accidental to the modifier being pressed.
80 m_ignoreZoom = false;
81 }
82 m_lastWheelEventUnmodified = false;
83 if (m_ignoreZoom) {
84 // unset the modifier so the view scrollbars can handle the scroll
85 // event and produce normal, not accelerated scrolling
86 modState &= ~modifier;
87 e->setModifiers(modState);
88 }
89 } else {
90 // state is reset after any wheel event without the zoom modifier
91 m_lastWheelEventUnmodified = true;
92 m_ignoreZoom = false;
93 }
94 m_lastWheelEvent.start();
95
96 // inform the caller whether this event is allowed to trigger text zooming.
97 return !m_ignoreZoom && modState == modifier;
98 }
99
100 protected:
101 QElapsedTimer m_lastWheelEvent;
102 bool m_ignoreZoom = false;
103 bool m_lastWheelEventUnmodified = false;
104 };
105
KateViewInternal(KTextEditor::ViewPrivate * view)106 KateViewInternal::KateViewInternal(KTextEditor::ViewPrivate *view)
107 : QWidget(view)
108 , editSessionNumber(0)
109 , editIsRunning(false)
110 , m_view(view)
111 , m_cursor(doc()->buffer(), KTextEditor::Cursor(0, 0), Kate::TextCursor::MoveOnInsert)
112 , m_mouse()
113 , m_possibleTripleClick(false)
114 , m_completionItemExpanded(false)
115 , m_bm(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand))
116 , m_bmStart(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand))
117 , m_bmEnd(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand))
118 , m_bmLastFlashPos(doc()->newMovingCursor(KTextEditor::Cursor::invalid()))
119
120 // folding marker
121 , m_fmStart(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand))
122 , m_fmEnd(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand))
123
124 , m_dummy(nullptr)
125
126 // stay on cursor will avoid that the view scroll around on press return at beginning
127 , m_startPos(doc()->buffer(), KTextEditor::Cursor(0, 0), Kate::TextCursor::StayOnInsert)
128
129 , m_visibleLineCount(0)
130 , m_madeVisible(false)
131 , m_shiftKeyPressed(false)
132 , m_autoCenterLines(0)
133 , m_minLinesVisible(0)
134 , m_selChangedByUser(false)
135 , m_selectAnchor(-1, -1)
136 , m_selectionMode(Default)
137 , m_layoutCache(new KateLayoutCache(renderer(), this))
138 , m_preserveX(false)
139 , m_preservedX(0)
140 , m_cachedMaxStartPos(-1, -1)
141 , m_dragScrollTimer(this)
142 , m_scrollTimer(this)
143 , m_cursorTimer(this)
144 , m_textHintTimer(this)
145 , m_textHintDelay(500)
146 , m_textHintPos(-1, -1)
147 , m_imPreeditRange(nullptr)
148 {
149 // setup input modes
150 Q_ASSERT(m_inputModes.size() == KTextEditor::EditorPrivate::self()->inputModeFactories().size());
151 m_inputModes[KTextEditor::View::NormalInputMode].reset(
152 KTextEditor::EditorPrivate::self()->inputModeFactories()[KTextEditor::View::NormalInputMode]->createInputMode(this));
153 m_inputModes[KTextEditor::View::ViInputMode].reset(
154 KTextEditor::EditorPrivate::self()->inputModeFactories()[KTextEditor::View::ViInputMode]->createInputMode(this));
155 m_currentInputMode = m_inputModes[KTextEditor::View::NormalInputMode].get();
156
157 setMinimumSize(0, 0);
158 setAttribute(Qt::WA_OpaquePaintEvent);
159 setAttribute(Qt::WA_InputMethodEnabled);
160
161 // invalidate m_selectionCached.start(), or keyb selection is screwed initially
162 m_selectionCached = KTextEditor::Range::invalid();
163
164 // bracket markers are only for this view and should not be printed
165 m_bm->setView(m_view);
166 m_bmStart->setView(m_view);
167 m_bmEnd->setView(m_view);
168 m_bm->setAttributeOnlyForViews(true);
169 m_bmStart->setAttributeOnlyForViews(true);
170 m_bmEnd->setAttributeOnlyForViews(true);
171
172 // use z depth defined in moving ranges interface
173 m_bm->setZDepth(-1000.0);
174 m_bmStart->setZDepth(-1000.0);
175 m_bmEnd->setZDepth(-1000.0);
176
177 // update mark attributes
178 updateBracketMarkAttributes();
179
180 //
181 // scrollbar for lines
182 //
183 m_lineScroll = new KateScrollBar(Qt::Vertical, this);
184 m_lineScroll->show();
185 m_lineScroll->setTracking(true);
186 m_lineScroll->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
187
188 // Hijack the line scroller's controls, so we can scroll nicely for word-wrap
189 connect(m_lineScroll, &KateScrollBar::actionTriggered, this, &KateViewInternal::scrollAction);
190
191 auto viewScrollLinesSlot = qOverload<int>(&KateViewInternal::scrollLines);
192 connect(m_lineScroll, &KateScrollBar::sliderMoved, this, viewScrollLinesSlot);
193 connect(m_lineScroll, &KateScrollBar::sliderMMBMoved, this, viewScrollLinesSlot);
194 connect(m_lineScroll, &KateScrollBar::valueChanged, this, viewScrollLinesSlot);
195
196 //
197 // scrollbar for columns
198 //
199 m_columnScroll = new QScrollBar(Qt::Horizontal, m_view);
200 m_scroller = QScroller::scroller(this);
201 QScrollerProperties prop;
202 prop.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.3);
203 prop.setScrollMetric(QScrollerProperties::MaximumVelocity, 1);
204 prop.setScrollMetric(QScrollerProperties::AcceleratingFlickMaximumTime, 0.2); // Workaround for QTBUG-88249 (non-flick gestures recognized as accelerating flick)
205 prop.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
206 prop.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
207 prop.setScrollMetric(QScrollerProperties::DragStartDistance, 0.0);
208 m_scroller->setScrollerProperties(prop);
209 m_scroller->grabGesture(this);
210
211 if (m_view->dynWordWrap()) {
212 m_columnScroll->hide();
213 } else {
214 m_columnScroll->show();
215 }
216
217 m_columnScroll->setTracking(true);
218 m_startX = 0;
219
220 connect(m_columnScroll, &QScrollBar::valueChanged, this, &KateViewInternal::scrollColumns);
221
222 // bottom corner box
223 m_dummy = new QWidget(m_view);
224 m_dummy->setFixedSize(m_lineScroll->width(), m_columnScroll->sizeHint().height());
225 m_dummy->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
226
227 if (m_view->dynWordWrap()) {
228 m_dummy->hide();
229 } else {
230 m_dummy->show();
231 }
232
233 cache()->setWrap(m_view->dynWordWrap());
234
235 //
236 // iconborder ;)
237 //
238 m_leftBorder = new KateIconBorder(this, m_view);
239 m_leftBorder->show();
240
241 // update view if folding ranges change
242 connect(&m_view->textFolding(), &Kate::TextFolding::foldingRangesChanged, this, &KateViewInternal::slotRegionVisibilityChanged);
243
244 m_displayCursor.setPosition(0, 0);
245
246 setAcceptDrops(true);
247
248 m_zoomEventFilter.reset(new ZoomEventFilter());
249 // event filter
250 installEventFilter(this);
251
252 // set initial cursor
253 m_mouseCursor = Qt::IBeamCursor;
254 setCursor(m_mouseCursor);
255
256 // call mouseMoveEvent also if no mouse button is pressed
257 setMouseTracking(true);
258
259 m_dragInfo.state = diNone;
260
261 // timers
262 connect(&m_dragScrollTimer, &QTimer::timeout, this, &KateViewInternal::doDragScroll);
263
264 connect(&m_scrollTimer, &QTimer::timeout, this, &KateViewInternal::scrollTimeout);
265
266 connect(&m_cursorTimer, &QTimer::timeout, this, &KateViewInternal::cursorTimeout);
267
268 connect(&m_textHintTimer, &QTimer::timeout, this, &KateViewInternal::textHintTimeout);
269
270 // selection changed to set anchor
271 connect(m_view, &KTextEditor::ViewPrivate::selectionChanged, this, &KateViewInternal::viewSelectionChanged);
272
273 #ifndef QT_NO_ACCESSIBILITY
274 QAccessible::installFactory(accessibleInterfaceFactory);
275 #endif
276 connect(doc(), &KTextEditor::DocumentPrivate::textInsertedRange, this, &KateViewInternal::documentTextInserted);
277 connect(doc(), &KTextEditor::DocumentPrivate::textRemoved, this, &KateViewInternal::documentTextRemoved);
278
279 // update is called in KTextEditor::ViewPrivate, after construction and layout is over
280 // but before any other kateviewinternal call
281 }
282
~KateViewInternal()283 KateViewInternal::~KateViewInternal()
284 {
285 // delete text animation object here, otherwise it updates the view in its destructor
286 delete m_textAnimation;
287
288 #ifndef QT_NO_ACCESSIBILITY
289 QAccessible::removeFactory(accessibleInterfaceFactory);
290 #endif
291 }
292
prepareForDynWrapChange()293 void KateViewInternal::prepareForDynWrapChange()
294 {
295 // Which is the current view line?
296 m_wrapChangeViewLine = cache()->displayViewLine(m_displayCursor, true);
297 }
298
dynWrapChanged()299 void KateViewInternal::dynWrapChanged()
300 {
301 m_dummy->setFixedSize(m_lineScroll->width(), m_columnScroll->sizeHint().height());
302 if (view()->dynWordWrap()) {
303 m_columnScroll->hide();
304 m_dummy->hide();
305
306 } else {
307 // column scrollbar + bottom corner box
308 m_columnScroll->show();
309 m_dummy->show();
310 }
311
312 cache()->setWrap(view()->dynWordWrap());
313 updateView();
314
315 if (view()->dynWordWrap()) {
316 scrollColumns(0);
317 }
318
319 // Determine where the cursor should be to get the cursor on the same view line
320 if (m_wrapChangeViewLine != -1) {
321 KTextEditor::Cursor newStart = viewLineOffset(m_displayCursor, -m_wrapChangeViewLine);
322 makeVisible(newStart, newStart.column(), true);
323
324 } else {
325 update();
326 }
327 }
328
endPos() const329 KTextEditor::Cursor KateViewInternal::endPos() const
330 {
331 // Hrm, no lines laid out at all??
332 if (!cache()->viewCacheLineCount()) {
333 return KTextEditor::Cursor();
334 }
335
336 for (int i = qMin(linesDisplayed() - 1, cache()->viewCacheLineCount() - 1); i >= 0; i--) {
337 const KateTextLayout &thisLine = cache()->viewLine(i);
338
339 if (thisLine.line() == -1) {
340 continue;
341 }
342
343 if (thisLine.virtualLine() >= view()->textFolding().visibleLines()) {
344 // Cache is too out of date
345 return KTextEditor::Cursor(view()->textFolding().visibleLines() - 1,
346 doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1)));
347 }
348
349 return KTextEditor::Cursor(thisLine.virtualLine(), thisLine.wrap() ? thisLine.endCol() - 1 : thisLine.endCol());
350 }
351
352 // can happen, if view is still invisible
353 return KTextEditor::Cursor();
354 }
355
endLine() const356 int KateViewInternal::endLine() const
357 {
358 return endPos().line();
359 }
360
yToKateTextLayout(int y) const361 KateTextLayout KateViewInternal::yToKateTextLayout(int y) const
362 {
363 if (y < 0 || y > size().height()) {
364 return KateTextLayout::invalid();
365 }
366
367 int range = y / renderer()->lineHeight();
368
369 // lineRanges is always bigger than 0, after the initial updateView call
370 if (range >= 0 && range < cache()->viewCacheLineCount()) {
371 return cache()->viewLine(range);
372 }
373
374 return KateTextLayout::invalid();
375 }
376
lineToY(int viewLine) const377 int KateViewInternal::lineToY(int viewLine) const
378 {
379 return (viewLine - startLine()) * renderer()->lineHeight();
380 }
381
slotIncFontSizes(qreal step)382 void KateViewInternal::slotIncFontSizes(qreal step)
383 {
384 renderer()->increaseFontSizes(step);
385 }
386
slotDecFontSizes(qreal step)387 void KateViewInternal::slotDecFontSizes(qreal step)
388 {
389 renderer()->decreaseFontSizes(step);
390 }
391
slotResetFontSizes()392 void KateViewInternal::slotResetFontSizes()
393 {
394 renderer()->resetFontSizes();
395 }
396
397 /**
398 * Line is the real line number to scroll to.
399 */
scrollLines(int line)400 void KateViewInternal::scrollLines(int line)
401 {
402 KTextEditor::Cursor newPos(line, 0);
403 scrollPos(newPos);
404 }
405
406 // This can scroll less than one true line
scrollViewLines(int offset)407 void KateViewInternal::scrollViewLines(int offset)
408 {
409 KTextEditor::Cursor c = viewLineOffset(startPos(), offset);
410 scrollPos(c);
411
412 bool blocked = m_lineScroll->blockSignals(true);
413 m_lineScroll->setValue(startLine());
414 m_lineScroll->blockSignals(blocked);
415 }
416
scrollAction(int action)417 void KateViewInternal::scrollAction(int action)
418 {
419 switch (action) {
420 case QAbstractSlider::SliderSingleStepAdd:
421 scrollNextLine();
422 break;
423
424 case QAbstractSlider::SliderSingleStepSub:
425 scrollPrevLine();
426 break;
427
428 case QAbstractSlider::SliderPageStepAdd:
429 scrollNextPage();
430 break;
431
432 case QAbstractSlider::SliderPageStepSub:
433 scrollPrevPage();
434 break;
435
436 case QAbstractSlider::SliderToMinimum:
437 top_home();
438 break;
439
440 case QAbstractSlider::SliderToMaximum:
441 bottom_end();
442 break;
443 }
444 }
445
scrollNextPage()446 void KateViewInternal::scrollNextPage()
447 {
448 scrollViewLines(qMax(linesDisplayed() - 1, 0));
449 }
450
scrollPrevPage()451 void KateViewInternal::scrollPrevPage()
452 {
453 scrollViewLines(-qMax(linesDisplayed() - 1, 0));
454 }
455
scrollPrevLine()456 void KateViewInternal::scrollPrevLine()
457 {
458 scrollViewLines(-1);
459 }
460
scrollNextLine()461 void KateViewInternal::scrollNextLine()
462 {
463 scrollViewLines(1);
464 }
465
maxStartPos(bool changed)466 KTextEditor::Cursor KateViewInternal::maxStartPos(bool changed)
467 {
468 cache()->setAcceptDirtyLayouts(true);
469
470 if (m_cachedMaxStartPos.line() == -1 || changed) {
471 KTextEditor::Cursor end(view()->textFolding().visibleLines() - 1,
472 doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1)));
473
474 if (view()->config()->scrollPastEnd()) {
475 m_cachedMaxStartPos = viewLineOffset(end, -m_minLinesVisible);
476 } else {
477 m_cachedMaxStartPos = viewLineOffset(end, -(linesDisplayed() - 1));
478 }
479 }
480
481 cache()->setAcceptDirtyLayouts(false);
482
483 return m_cachedMaxStartPos;
484 }
485
486 // c is a virtual cursor
scrollPos(KTextEditor::Cursor & c,bool force,bool calledExternally,bool emitSignals)487 void KateViewInternal::scrollPos(KTextEditor::Cursor &c, bool force, bool calledExternally, bool emitSignals)
488 {
489 if (!force && ((!view()->dynWordWrap() && c.line() == startLine()) || c == startPos())) {
490 return;
491 }
492
493 if (c.line() < 0) {
494 c.setLine(0);
495 }
496
497 KTextEditor::Cursor limit = maxStartPos();
498 if (c > limit) {
499 c = limit;
500
501 // Re-check we're not just scrolling to the same place
502 if (!force && ((!view()->dynWordWrap() && c.line() == startLine()) || c == startPos())) {
503 return;
504 }
505 }
506
507 int viewLinesScrolled = 0;
508
509 // only calculate if this is really used and useful, could be wrong here, please recheck
510 // for larger scrolls this makes 2-4 seconds difference on my xeon with dyn. word wrap on
511 // try to get it really working ;)
512 bool viewLinesScrolledUsable = !force && (c.line() >= startLine() - linesDisplayed() - 1) && (c.line() <= endLine() + linesDisplayed() + 1);
513
514 if (viewLinesScrolledUsable) {
515 viewLinesScrolled = cache()->displayViewLine(c);
516 }
517
518 m_startPos.setPosition(c);
519
520 // set false here but reversed if we return to makeVisible
521 m_madeVisible = false;
522
523 if (viewLinesScrolledUsable) {
524 int lines = linesDisplayed();
525 if (view()->textFolding().visibleLines() < lines) {
526 KTextEditor::Cursor end(view()->textFolding().visibleLines() - 1,
527 doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1)));
528 lines = qMin(linesDisplayed(), cache()->displayViewLine(end) + 1);
529 }
530
531 Q_ASSERT(lines >= 0);
532
533 if (!calledExternally && qAbs(viewLinesScrolled) < lines &&
534 // NOTE: on some machines we must update if the floating widget is visible
535 // otherwise strange painting bugs may occur during scrolling...
536 !((view()->m_messageWidgets[KTextEditor::Message::TopInView] && view()->m_messageWidgets[KTextEditor::Message::TopInView]->isVisible())
537 || (view()->m_messageWidgets[KTextEditor::Message::CenterInView] && view()->m_messageWidgets[KTextEditor::Message::CenterInView]->isVisible())
538 || (view()->m_messageWidgets[KTextEditor::Message::BottomInView] && view()->m_messageWidgets[KTextEditor::Message::BottomInView]->isVisible()))) {
539 updateView(false, viewLinesScrolled);
540
541 int scrollHeight = -(viewLinesScrolled * (int)renderer()->lineHeight());
542
543 // scroll excluding child widgets (floating notifications)
544 scroll(0, scrollHeight, rect());
545 m_leftBorder->scroll(0, scrollHeight);
546
547 if (emitSignals) {
548 Q_EMIT view()->verticalScrollPositionChanged(m_view, c);
549 Q_EMIT view()->displayRangeChanged(m_view);
550 }
551 return;
552 }
553 }
554
555 updateView();
556 update();
557 m_leftBorder->update();
558 if (emitSignals) {
559 Q_EMIT view()->verticalScrollPositionChanged(m_view, c);
560 Q_EMIT view()->displayRangeChanged(m_view);
561 }
562 }
563
scrollColumns(int x)564 void KateViewInternal::scrollColumns(int x)
565 {
566 if (x < 0) {
567 x = 0;
568 }
569
570 if (x > m_columnScroll->maximum()) {
571 x = m_columnScroll->maximum();
572 }
573
574 if (x == startX()) {
575 return;
576 }
577
578 int dx = startX() - x;
579 m_startX = x;
580
581 if (qAbs(dx) < width()) {
582 // scroll excluding child widgets (floating notifications)
583 scroll(dx, 0, rect());
584 } else {
585 update();
586 }
587
588 Q_EMIT view()->horizontalScrollPositionChanged(m_view);
589 Q_EMIT view()->displayRangeChanged(m_view);
590
591 bool blocked = m_columnScroll->blockSignals(true);
592 m_columnScroll->setValue(startX());
593 m_columnScroll->blockSignals(blocked);
594 }
595
596 // If changed is true, the lines that have been set dirty have been updated.
updateView(bool changed,int viewLinesScrolled)597 void KateViewInternal::updateView(bool changed, int viewLinesScrolled)
598 {
599 if (!isVisible() && !viewLinesScrolled && !changed) {
600 return; // When this view is not visible, don't do anything
601 }
602
603 view()->doc()->delayAutoReload(); // Don't reload while user scrolls around
604 bool blocked = m_lineScroll->blockSignals(true);
605
606 int wrapWidth = width();
607 if (view()->config()->dynWrapAtStaticMarker() && view()->config()->dynWordWrap()) {
608 // We need to transform char count to a pixel width, stolen from PrintPainter::updateCache()
609 QString s;
610 s.fill(QLatin1Char('5'), view()->doc()->config()->wordWrapAt());
611 wrapWidth = qMin(width(), static_cast<int>(renderer()->currentFontMetrics().boundingRect(s).width()));
612 }
613
614 if (wrapWidth != cache()->viewWidth()) {
615 cache()->setViewWidth(wrapWidth);
616 changed = true;
617 }
618
619 /* It was observed that height() could be negative here --
620 when the main Kate view has 0 as size (during creation),
621 and there frame around KateViewInternal. In which
622 case we'd set the view cache to 0 (or less!) lines, and
623 start allocating huge chunks of data, later. */
624 int newSize = (qMax(0, height()) / renderer()->lineHeight()) + 1;
625 cache()->updateViewCache(startPos(), newSize, viewLinesScrolled);
626 m_visibleLineCount = newSize;
627
628 KTextEditor::Cursor maxStart = maxStartPos(changed);
629 int maxLineScrollRange = maxStart.line();
630 if (view()->dynWordWrap() && maxStart.column() != 0) {
631 maxLineScrollRange++;
632 }
633 m_lineScroll->setRange(0, maxLineScrollRange);
634
635 m_lineScroll->setValue(startLine());
636 m_lineScroll->setSingleStep(1);
637 m_lineScroll->setPageStep(qMax(0, height()) / renderer()->lineHeight());
638 m_lineScroll->blockSignals(blocked);
639
640 KateViewConfig::ScrollbarMode show_scrollbars = static_cast<KateViewConfig::ScrollbarMode>(view()->config()->showScrollbars());
641
642 bool visible = ((show_scrollbars == KateViewConfig::AlwaysOn) || ((show_scrollbars == KateViewConfig::ShowWhenNeeded) && (maxLineScrollRange != 0)));
643 bool visible_dummy = visible;
644
645 m_lineScroll->setVisible(visible);
646
647 if (!view()->dynWordWrap()) {
648 int max = maxLen(startLine()) - width();
649 if (max < 0) {
650 max = 0;
651 }
652
653 // if we lose the ability to scroll horizontally, move view to the far-left
654 if (max == 0) {
655 scrollColumns(0);
656 }
657
658 blocked = m_columnScroll->blockSignals(true);
659
660 // disable scrollbar
661 m_columnScroll->setDisabled(max == 0);
662
663 visible = ((show_scrollbars == KateViewConfig::AlwaysOn) || ((show_scrollbars == KateViewConfig::ShowWhenNeeded) && (max != 0)));
664 visible_dummy &= visible;
665 m_columnScroll->setVisible(visible);
666
667 m_columnScroll->setRange(0, max + (renderer()->spaceWidth() / 2)); // Add some space for the caret at EOL
668
669 m_columnScroll->setValue(startX());
670
671 // Approximate linescroll
672 m_columnScroll->setSingleStep(renderer()->currentFontMetrics().horizontalAdvance(QLatin1Char('a')));
673 m_columnScroll->setPageStep(width());
674
675 m_columnScroll->blockSignals(blocked);
676 } else {
677 visible_dummy = false;
678 }
679
680 m_dummy->setVisible(visible_dummy);
681
682 if (changed) {
683 updateDirty();
684 }
685 }
686
687 /**
688 * this function ensures a certain location is visible on the screen.
689 * if endCol is -1, ignore making the columns visible.
690 */
makeVisible(const KTextEditor::Cursor c,int endCol,bool force,bool center,bool calledExternally)691 void KateViewInternal::makeVisible(const KTextEditor::Cursor c, int endCol, bool force, bool center, bool calledExternally)
692 {
693 // qCDebug(LOG_KTE) << "MakeVisible start " << startPos() << " end " << endPos() << " -> request: " << c;// , new start [" << scroll.line << "," <<
694 // scroll.col << "] lines " << (linesDisplayed() - 1) << " height " << height(); if the line is in a folded region, unfold all the way up if (
695 // doc()->foldingTree()->findNodeForLine( c.line )->visible )
696 // qCDebug(LOG_KTE)<<"line ("<<c.line<<") should be visible";
697
698 const int lnDisp = linesDisplayed();
699 const int viewLine = cache()->displayViewLine(c, true);
700 const bool curBelowScreen = (viewLine == -2);
701
702 if (force) {
703 KTextEditor::Cursor scroll = c;
704 scrollPos(scroll, force, calledExternally);
705 } else if (center && (c < startPos() || c > endPos())) {
706 KTextEditor::Cursor scroll = viewLineOffset(c, -int(lnDisp) / 2);
707 scrollPos(scroll, false, calledExternally);
708 } else if ((viewLine >= (lnDisp - m_minLinesVisible)) || (curBelowScreen)) {
709 KTextEditor::Cursor scroll = viewLineOffset(c, -(lnDisp - m_minLinesVisible - 1));
710 scrollPos(scroll, false, calledExternally);
711 } else if (c < viewLineOffset(startPos(), m_minLinesVisible)) {
712 KTextEditor::Cursor scroll = viewLineOffset(c, -m_minLinesVisible);
713 scrollPos(scroll, false, calledExternally);
714 } else {
715 // Check to see that we're not showing blank lines
716 KTextEditor::Cursor max = maxStartPos();
717 if (startPos() > max) {
718 scrollPos(max, max.column(), calledExternally);
719 }
720 }
721
722 if (!view()->dynWordWrap() && (endCol != -1 || view()->wrapCursor())) {
723 KTextEditor::Cursor rc = toRealCursor(c);
724 int sX = renderer()->cursorToX(cache()->textLayout(rc), rc, !view()->wrapCursor());
725
726 int sXborder = sX - 8;
727 if (sXborder < 0) {
728 sXborder = 0;
729 }
730
731 if (sX < startX()) {
732 scrollColumns(sXborder);
733 } else if (sX > startX() + width()) {
734 scrollColumns(sX - width() + 8);
735 }
736 }
737
738 m_madeVisible = !force;
739
740 #ifndef QT_NO_ACCESSIBILITY
741 // FIXME -- is this needed?
742 // QAccessible::updateAccessibility(this, KateCursorAccessible::ChildId, QAccessible::Focus);
743 #endif
744 }
745
slotRegionVisibilityChanged()746 void KateViewInternal::slotRegionVisibilityChanged()
747 {
748 qCDebug(LOG_KTE);
749
750 // ensure the layout cache is ok for the updateCursor calls below
751 // without the updateView() the view will jump to the bottom on hiding blocks after
752 // change cfb0af25bdfac0d8f86b42db0b34a6bc9f9a361e
753 cache()->clear();
754 updateView();
755
756 m_cachedMaxStartPos.setLine(-1);
757 KTextEditor::Cursor max = maxStartPos();
758 if (startPos() > max) {
759 scrollPos(max, false, false, false /* don't emit signals! */);
760 }
761
762 // if text was folded: make sure the cursor is on a visible line
763 qint64 foldedRangeId = -1;
764 if (!view()->textFolding().isLineVisible(m_cursor.line(), &foldedRangeId)) {
765 KTextEditor::Range foldingRange = view()->textFolding().foldingRange(foldedRangeId);
766 Q_ASSERT(foldingRange.start().isValid());
767
768 // set cursor to start of folding region
769 updateCursor(foldingRange.start(), true);
770 } else {
771 // force an update of the cursor, since otherwise the m_displayCursor
772 // line may be below the total amount of visible lines.
773 updateCursor(m_cursor, true);
774 }
775
776 updateView();
777 update();
778 m_leftBorder->update();
779
780 // emit signals here, scrollPos has this disabled, to ensure we do this after all stuff is updated!
781 Q_EMIT view()->verticalScrollPositionChanged(m_view, max);
782 Q_EMIT view()->displayRangeChanged(m_view);
783 }
784
slotRegionBeginEndAddedRemoved(unsigned int)785 void KateViewInternal::slotRegionBeginEndAddedRemoved(unsigned int)
786 {
787 qCDebug(LOG_KTE);
788 // FIXME: performance problem
789 m_leftBorder->update();
790 }
791
showEvent(QShowEvent * e)792 void KateViewInternal::showEvent(QShowEvent *e)
793 {
794 updateView();
795
796 QWidget::showEvent(e);
797 }
798
attributeAt(const KTextEditor::Cursor position) const799 KTextEditor::Attribute::Ptr KateViewInternal::attributeAt(const KTextEditor::Cursor position) const
800 {
801 KTextEditor::Attribute::Ptr attrib(new KTextEditor::Attribute());
802
803 Kate::TextLine kateLine = doc()->kateTextLine(position.line());
804 if (!kateLine) {
805 return attrib;
806 }
807
808 *attrib = *m_view->renderer()->attribute(kateLine->attribute(position.column()));
809
810 return attrib;
811 }
812
linesDisplayed() const813 int KateViewInternal::linesDisplayed() const
814 {
815 int h = height();
816
817 // catch zero heights, even if should not happen
818 int fh = qMax(1, renderer()->lineHeight());
819
820 // default to 1, there is always one line around....
821 // too many places calc with linesDisplayed() - 1
822 return qMax(1, (h - (h % fh)) / fh);
823 }
824
cursorToCoordinate(const KTextEditor::Cursor cursor,bool realCursor,bool includeBorder) const825 QPoint KateViewInternal::cursorToCoordinate(const KTextEditor::Cursor cursor, bool realCursor, bool includeBorder) const
826 {
827 if (cursor.line() >= doc()->lines()) {
828 return QPoint(-1, -1);
829 }
830
831 int viewLine = cache()->displayViewLine(realCursor ? toVirtualCursor(cursor) : cursor, true);
832
833 if (viewLine < 0 || viewLine >= cache()->viewCacheLineCount()) {
834 return QPoint(-1, -1);
835 }
836
837 const int y = (int)viewLine * renderer()->lineHeight();
838
839 KateTextLayout layout = cache()->viewLine(viewLine);
840
841 if (cursor.column() > doc()->lineLength(cursor.line())) {
842 return QPoint(-1, -1);
843 }
844
845 int x = 0;
846
847 // only set x value if we have a valid layout (bug #171027)
848 if (layout.isValid()) {
849 x = (int)layout.lineLayout().cursorToX(cursor.column());
850 }
851 // else
852 // qCDebug(LOG_KTE) << "Invalid Layout";
853
854 if (includeBorder) {
855 x += m_leftBorder->width();
856 }
857
858 x -= startX();
859
860 return QPoint(x, y);
861 }
862
cursorCoordinates(bool includeBorder) const863 QPoint KateViewInternal::cursorCoordinates(bool includeBorder) const
864 {
865 return cursorToCoordinate(m_displayCursor, false, includeBorder);
866 }
867
findMatchingBracket()868 KTextEditor::Cursor KateViewInternal::findMatchingBracket()
869 {
870 KTextEditor::Cursor c;
871
872 if (!m_bm->toRange().isValid()) {
873 return KTextEditor::Cursor::invalid();
874 }
875
876 Q_ASSERT(m_bmEnd->toRange().isValid());
877 Q_ASSERT(m_bmStart->toRange().isValid());
878
879 // For e.g. the text "{|}" (where | is the cursor), m_bmStart is equal to [ (0, 0) -> (0, 1) ]
880 // and the closing bracket is in (0, 1). Thus, we check m_bmEnd first.
881 if (m_bmEnd->toRange().contains(m_cursor) || m_bmEnd->end() == m_cursor.toCursor()) {
882 c = m_bmStart->start();
883 } else if (m_bmStart->toRange().contains(m_cursor) || m_bmStart->end() == m_cursor.toCursor()) {
884 c = m_bmEnd->end();
885 // We need to adjust the cursor position in case of override mode, BUG-402594
886 if (doc()->config()->ovr()) {
887 c.setColumn(c.column() - 1);
888 }
889 } else {
890 // should never happen: a range exists, but the cursor position is
891 // neither at the start nor at the end...
892 return KTextEditor::Cursor::invalid();
893 }
894
895 return c;
896 }
897
898 class CalculatingCursor
899 {
900 public:
901 // These constructors constrain their arguments to valid positions
902 // before only the third one did, but that leads to crashes
903 // see bug 227449
CalculatingCursor(KateViewInternal * vi)904 CalculatingCursor(KateViewInternal *vi)
905 : m_vi(vi)
906 {
907 makeValid();
908 }
909
CalculatingCursor(KateViewInternal * vi,const KTextEditor::Cursor c)910 CalculatingCursor(KateViewInternal *vi, const KTextEditor::Cursor c)
911 : m_cursor(c)
912 , m_vi(vi)
913 {
914 makeValid();
915 }
916
CalculatingCursor(KateViewInternal * vi,int line,int col)917 CalculatingCursor(KateViewInternal *vi, int line, int col)
918 : m_cursor(line, col)
919 , m_vi(vi)
920 {
921 makeValid();
922 }
923
~CalculatingCursor()924 virtual ~CalculatingCursor()
925 {
926 }
927
line() const928 int line() const
929 {
930 return m_cursor.line();
931 }
932
column() const933 int column() const
934 {
935 return m_cursor.column();
936 }
937
operator KTextEditor::Cursor() const938 operator KTextEditor::Cursor() const
939 {
940 return m_cursor;
941 }
942
943 virtual CalculatingCursor &operator+=(int n) = 0;
944
945 virtual CalculatingCursor &operator-=(int n) = 0;
946
operator ++()947 CalculatingCursor &operator++()
948 {
949 return operator+=(1);
950 }
951
operator --()952 CalculatingCursor &operator--()
953 {
954 return operator-=(1);
955 }
956
makeValid()957 void makeValid()
958 {
959 m_cursor.setLine(qBound(0, line(), int(doc()->lines() - 1)));
960 if (view()->wrapCursor()) {
961 m_cursor.setColumn(qBound(0, column(), doc()->lineLength(line())));
962 } else {
963 m_cursor.setColumn(qMax(0, column()));
964 }
965 Q_ASSERT(valid());
966 }
967
toEdge(KateViewInternal::Bias bias)968 void toEdge(KateViewInternal::Bias bias)
969 {
970 if (bias == KateViewInternal::left) {
971 m_cursor.setColumn(0);
972 } else if (bias == KateViewInternal::right) {
973 m_cursor.setColumn(doc()->lineLength(line()));
974 }
975 }
976
atEdge() const977 bool atEdge() const
978 {
979 return atEdge(KateViewInternal::left) || atEdge(KateViewInternal::right);
980 }
981
atEdge(KateViewInternal::Bias bias) const982 bool atEdge(KateViewInternal::Bias bias) const
983 {
984 switch (bias) {
985 case KateViewInternal::left:
986 return column() == 0;
987 case KateViewInternal::none:
988 return atEdge();
989 case KateViewInternal::right:
990 return column() >= doc()->lineLength(line());
991 default:
992 Q_ASSERT(false);
993 return false;
994 }
995 }
996
997 protected:
valid() const998 bool valid() const
999 {
1000 return line() >= 0 && line() < doc()->lines() && column() >= 0 && (!view()->wrapCursor() || column() <= doc()->lineLength(line()));
1001 }
view()1002 KTextEditor::ViewPrivate *view()
1003 {
1004 return m_vi->m_view;
1005 }
view() const1006 const KTextEditor::ViewPrivate *view() const
1007 {
1008 return m_vi->m_view;
1009 }
doc()1010 KTextEditor::DocumentPrivate *doc()
1011 {
1012 return view()->doc();
1013 }
doc() const1014 const KTextEditor::DocumentPrivate *doc() const
1015 {
1016 return view()->doc();
1017 }
1018 KTextEditor::Cursor m_cursor;
1019 KateViewInternal *m_vi;
1020 };
1021
1022 class BoundedCursor : public CalculatingCursor
1023 {
1024 public:
BoundedCursor(KateViewInternal * vi)1025 BoundedCursor(KateViewInternal *vi)
1026 : CalculatingCursor(vi)
1027 {
1028 }
BoundedCursor(KateViewInternal * vi,const KTextEditor::Cursor c)1029 BoundedCursor(KateViewInternal *vi, const KTextEditor::Cursor c)
1030 : CalculatingCursor(vi, c)
1031 {
1032 }
BoundedCursor(KateViewInternal * vi,int line,int col)1033 BoundedCursor(KateViewInternal *vi, int line, int col)
1034 : CalculatingCursor(vi, line, col)
1035 {
1036 }
operator +=(int n)1037 CalculatingCursor &operator+=(int n) override
1038 {
1039 KateLineLayoutPtr thisLine = m_vi->cache()->line(line());
1040 if (!thisLine->isValid()) {
1041 qCWarning(LOG_KTE) << "Did not retrieve valid layout for line " << line();
1042 return *this;
1043 }
1044
1045 const bool wrapCursor = view()->wrapCursor();
1046 int maxColumn = -1;
1047 if (n >= 0) {
1048 for (int i = 0; i < n; i++) {
1049 if (column() >= thisLine->length()) {
1050 if (wrapCursor) {
1051 break;
1052
1053 } else if (view()->dynWordWrap()) {
1054 // Don't go past the edge of the screen in dynamic wrapping mode
1055 if (maxColumn == -1) {
1056 maxColumn = thisLine->length() + ((m_vi->width() - thisLine->widthOfLastLine()) / m_vi->renderer()->spaceWidth()) - 1;
1057 }
1058
1059 if (column() >= maxColumn) {
1060 m_cursor.setColumn(maxColumn);
1061 break;
1062 }
1063
1064 m_cursor.setColumn(column() + 1);
1065
1066 } else {
1067 m_cursor.setColumn(column() + 1);
1068 }
1069
1070 } else {
1071 m_cursor.setColumn(thisLine->layout()->nextCursorPosition(column()));
1072 }
1073 }
1074 } else {
1075 for (int i = 0; i > n; i--) {
1076 if (column() >= thisLine->length()) {
1077 m_cursor.setColumn(column() - 1);
1078 } else if (column() == 0) {
1079 break;
1080 } else {
1081 m_cursor.setColumn(thisLine->layout()->previousCursorPosition(column()));
1082 }
1083 }
1084 }
1085
1086 Q_ASSERT(valid());
1087 return *this;
1088 }
operator -=(int n)1089 CalculatingCursor &operator-=(int n) override
1090 {
1091 return operator+=(-n);
1092 }
1093 };
1094
1095 class WrappingCursor : public CalculatingCursor
1096 {
1097 public:
WrappingCursor(KateViewInternal * vi)1098 WrappingCursor(KateViewInternal *vi)
1099 : CalculatingCursor(vi)
1100 {
1101 }
WrappingCursor(KateViewInternal * vi,const KTextEditor::Cursor c)1102 WrappingCursor(KateViewInternal *vi, const KTextEditor::Cursor c)
1103 : CalculatingCursor(vi, c)
1104 {
1105 }
WrappingCursor(KateViewInternal * vi,int line,int col)1106 WrappingCursor(KateViewInternal *vi, int line, int col)
1107 : CalculatingCursor(vi, line, col)
1108 {
1109 }
1110
operator +=(int n)1111 CalculatingCursor &operator+=(int n) override
1112 {
1113 KateLineLayoutPtr thisLine = m_vi->cache()->line(line());
1114 if (!thisLine->isValid()) {
1115 qCWarning(LOG_KTE) << "Did not retrieve a valid layout for line " << line();
1116 return *this;
1117 }
1118
1119 if (n >= 0) {
1120 for (int i = 0; i < n; i++) {
1121 if (column() >= thisLine->length()) {
1122 // Have come to the end of a line
1123 if (line() >= doc()->lines() - 1)
1124 // Have come to the end of the document
1125 {
1126 break;
1127 }
1128
1129 // Advance to the beginning of the next line
1130 m_cursor.setColumn(0);
1131 m_cursor.setLine(line() + 1);
1132
1133 // Retrieve the next text range
1134 thisLine = m_vi->cache()->line(line());
1135 if (!thisLine->isValid()) {
1136 qCWarning(LOG_KTE) << "Did not retrieve a valid layout for line " << line();
1137 return *this;
1138 }
1139
1140 continue;
1141 }
1142
1143 m_cursor.setColumn(thisLine->layout()->nextCursorPosition(column()));
1144 }
1145
1146 } else {
1147 for (int i = 0; i > n; i--) {
1148 if (column() == 0) {
1149 // Have come to the start of the document
1150 if (line() == 0) {
1151 break;
1152 }
1153
1154 // Start going back to the end of the last line
1155 m_cursor.setLine(line() - 1);
1156
1157 // Retrieve the next text range
1158 thisLine = m_vi->cache()->line(line());
1159 if (!thisLine->isValid()) {
1160 qCWarning(LOG_KTE) << "Did not retrieve a valid layout for line " << line();
1161 return *this;
1162 }
1163
1164 // Finish going back to the end of the last line
1165 m_cursor.setColumn(thisLine->length());
1166
1167 continue;
1168 }
1169
1170 if (column() > thisLine->length()) {
1171 m_cursor.setColumn(column() - 1);
1172 } else {
1173 m_cursor.setColumn(thisLine->layout()->previousCursorPosition(column()));
1174 }
1175 }
1176 }
1177
1178 Q_ASSERT(valid());
1179 return *this;
1180 }
operator -=(int n)1181 CalculatingCursor &operator-=(int n) override
1182 {
1183 return operator+=(-n);
1184 }
1185 };
1186
1187 /**
1188 * @brief The CamelCursor class
1189 *
1190 * This class allows for "camel humps" when moving the cursor
1191 * using Ctrl + Left / Right. Similarly, this will also get triggered
1192 * when you press Ctrl+Shift+Left/Right for selection and Ctrl+Del
1193 * Ctrl + backspace for deletion.
1194 *
1195 * It is absoloutely essential that if you move through a word in 'n'
1196 * jumps, you should be able to move back with exactly same 'n' movements
1197 * which you made when moving forward. Example:
1198 *
1199 * Word: KateViewInternal
1200 *
1201 * Moving cursor towards right while holding control will result in cursor
1202 * landing in the following places (assuming you start from column 0)
1203 *
1204 * | | |
1205 * KateViewInternal
1206 *
1207 * Moving the cursor back to get to the starting position should also
1208 * take exactly 3 movements:
1209 *
1210 * | | |
1211 * KateViewInternal
1212 *
1213 * In addition to simple camel case, this class also handles snake_case
1214 * capitalized snake, and mixtures of Camel + snake/underscore, for example
1215 * m_someMember. If a word has underscores in it, for example:
1216 *
1217 * snake_case_word
1218 *
1219 * the leading underscore is considered part of the word and thus a cursor
1220 * movement will land right after the underscore. Moving the cursor to end
1221 * for the above word should be like this:
1222 *
1223 * startpos: 0
1224 * | | |
1225 * snake_case_word
1226 *
1227 * When jumping back to startpos, exact same "spots" need to be hit on your way
1228 * back.
1229 *
1230 * If a word has multiple leading underscores: snake___case, the underscores will
1231 * be considered part of the word and thus a jump wil land us "after" the last underscore.
1232 *
1233 * There are some other variations in this, for example Capitalized words, or identifiers
1234 * with numbers in betweens. For such cases, All capitalized words are skipped in one go
1235 * till we are on some "non-capitalized" word. In this context, a number is a non-capitalized
1236 * word so will break the jump. Examples:
1237 *
1238 * | |
1239 * W1RD
1240 *
1241 * but for WORD_CAPITAL, following will happen:
1242 *
1243 * | |
1244 * WORD_CAPITAL
1245 *
1246 * The first case here is tricky to handle for reverse movement. I haven't noticed any
1247 * cases which the current implementation is unable to handle but there might be some.
1248 *
1249 * With languages like PHP where you have $ as part of the identifier, the cursor jump
1250 * will break "after" dollar. Consider: $phpVar, Following will happen:
1251 *
1252 * | | |
1253 * $phpVar
1254 *
1255 * And of course, the reverse will be exact opposite.
1256 *
1257 * Similar to PHP, with CSS Colors, jump will break on '#' charachter
1258 *
1259 * Please see the test cases testWordMovementSingleRow() for more examples/data.
1260 *
1261 * It is absoloutely essential to know that this class *only* gets triggered for
1262 * cursor movement if we are in a word.
1263 *
1264 * Note to bugfixer: If some bug occurs, before changing anything please add a test
1265 * case for the bug and make sure everything passes before and after. The test case
1266 * for this can be found autotests/src/camelcursortest.cpp.
1267 *
1268 * @author Waqar Ahmed <waqar.17a@gmail.com>
1269 */
1270 class CamelCursor : public CalculatingCursor
1271 {
1272 public:
CamelCursor(KateViewInternal * vi,const KTextEditor::Cursor c)1273 CamelCursor(KateViewInternal *vi, const KTextEditor::Cursor c)
1274 : CalculatingCursor(vi, c)
1275 {
1276 }
1277
operator +=(int n)1278 CalculatingCursor &operator+=(int n) override
1279 {
1280 KateLineLayoutPtr thisLine = m_vi->cache()->line(line());
1281 if (!thisLine->isValid()) {
1282 qCWarning(LOG_KTE) << "Did not retrieve valid layout for line " << line();
1283 return *this;
1284 }
1285
1286 if (n >= 0) {
1287 auto skipCaps = [](QStringView text, int &col) {
1288 int count = 0;
1289 while (col < text.size() && text.at(col).isUpper()) {
1290 ++count;
1291 ++col;
1292 }
1293 // if this is a letter, then it means we are in the
1294 // middle of a word, step back one position so that
1295 // we are at the last Cap letter
1296 // Otherwise, it's an all cap word
1297 if (count > 1 && col < text.size() && text.at(col).isLetterOrNumber()) {
1298 --col;
1299 }
1300 };
1301
1302 int jump = -1;
1303 int col = column();
1304 const QString &text = thisLine->textLine()->text();
1305
1306 if (col < text.size() && text.at(col).isUpper()) {
1307 skipCaps(text, col);
1308 }
1309
1310 for (int i = col; i < thisLine->length(); ++i) {
1311 if (text.at(i).isUpper() || !text.at(i).isLetterOrNumber()) {
1312 break;
1313 }
1314 ++col;
1315 }
1316
1317 // eat any '_' that are after the word BEFORE any space happens
1318 if (col < text.size() && text.at(col) == QLatin1Char('_')) {
1319 while (col < text.size() && text.at(col) == QLatin1Char('_')) {
1320 ++col;
1321 }
1322 }
1323
1324 // Underscores eaten, so now eat any spaces till next word
1325 if (col < text.size() && text.at(col).isSpace()) {
1326 while (col < text.size() && text.at(col).isSpace()) {
1327 ++col;
1328 }
1329 }
1330
1331 jump = col < 0 || (column() == col) ? (column() + 1) : col;
1332 m_cursor.setColumn(jump);
1333 } else {
1334 int jump = -1;
1335
1336 auto skipCapsRev = [](QStringView text, int &col) {
1337 int count = 0;
1338 while (col > 0 && text.at(col).isUpper()) {
1339 ++count;
1340 --col;
1341 }
1342
1343 // if more than one cap found, and current
1344 // column is not upper, we want to move ahead
1345 // to the upper
1346 if (count >= 1 && col >= 0 && !text.at(col).isUpper()) {
1347 ++col;
1348 }
1349 };
1350
1351 const QString &text = thisLine->textLine()->text();
1352 int col = std::min(column(), text.size() - 1);
1353 col = col - 1;
1354
1355 // skip any spaces
1356 if (col > 0 && text.at(col).isSpace()) {
1357 while (text.at(col).isSpace() && col > 0) {
1358 --col;
1359 }
1360 }
1361
1362 // Skip Underscores
1363 if (col > 0 && text.at(col) == QLatin1Char('_')) {
1364 while (col > 0 && text.at(col) == QLatin1Char('_')) {
1365 --col;
1366 }
1367 }
1368
1369 if (col > 0 && text.at(col).isUpper()) {
1370 skipCapsRev(text, col);
1371 }
1372
1373 for (int i = col; i > 0; --i) {
1374 if (text.at(i).isUpper() || !text.at(i).isLetterOrNumber()) {
1375 break;
1376 }
1377 --col;
1378 }
1379
1380 if (col >= 0 && !text.at(col).isLetterOrNumber()) {
1381 ++col;
1382 }
1383
1384 if (col < 0) {
1385 jump = 0;
1386 } else if (col == column() && column() > 0) {
1387 jump = column() - 1;
1388 } else {
1389 jump = col;
1390 }
1391
1392 m_cursor.setColumn(jump);
1393 }
1394
1395 Q_ASSERT(valid());
1396 return *this;
1397 }
1398
operator -=(int n)1399 CalculatingCursor &operator-=(int n) override
1400 {
1401 return operator+=(-n);
1402 }
1403 };
1404
moveChar(KateViewInternal::Bias bias,bool sel)1405 void KateViewInternal::moveChar(KateViewInternal::Bias bias, bool sel)
1406 {
1407 KTextEditor::Cursor c;
1408 if (view()->wrapCursor()) {
1409 c = WrappingCursor(this, m_cursor) += bias;
1410 } else {
1411 c = BoundedCursor(this, m_cursor) += bias;
1412 }
1413
1414 updateSelection(c, sel);
1415 updateCursor(c);
1416 }
1417
cursorPrevChar(bool sel)1418 void KateViewInternal::cursorPrevChar(bool sel)
1419 {
1420 if (!view()->wrapCursor() && m_cursor.column() == 0) {
1421 return;
1422 }
1423
1424 moveChar(KateViewInternal::left, sel);
1425 }
1426
cursorNextChar(bool sel)1427 void KateViewInternal::cursorNextChar(bool sel)
1428 {
1429 moveChar(KateViewInternal::right, sel);
1430 }
1431
wordPrev(bool sel)1432 void KateViewInternal::wordPrev(bool sel)
1433 {
1434 WrappingCursor c(this, m_cursor);
1435 // First we skip backwards all space.
1436 // Then we look up into which category the current position falls:
1437 // 1. a "word" character
1438 // 2. a "non-word" character (except space)
1439 // 3. the beginning of the line
1440 // and skip all preceding characters that fall into this class.
1441 // The code assumes that space is never part of the word character class.
1442
1443 KateHighlighting *h = doc()->highlight();
1444 if (!c.atEdge(left)) {
1445 while (!c.atEdge(left) && doc()->line(c.line())[c.column() - 1].isSpace()) {
1446 --c;
1447 }
1448 }
1449 if (c.atEdge(left)) {
1450 --c;
1451 } else if (h->isInWord(doc()->line(c.line())[c.column() - 1])) {
1452 if (doc()->config()->camelCursor()) {
1453 CamelCursor cc(this, m_cursor);
1454 --cc;
1455 updateSelection(cc, sel);
1456 updateCursor(cc);
1457 return;
1458 } else {
1459 while (!c.atEdge(left) && h->isInWord(doc()->line(c.line())[c.column() - 1])) {
1460 --c;
1461 }
1462 }
1463 } else {
1464 while (!c.atEdge(left)
1465 && !h->isInWord(doc()->line(c.line())[c.column() - 1])
1466 // in order to stay symmetric to wordLeft()
1467 // we must not skip space preceding a non-word sequence
1468 && !doc()->line(c.line())[c.column() - 1].isSpace()) {
1469 --c;
1470 }
1471 }
1472
1473 updateSelection(c, sel);
1474 updateCursor(c);
1475 }
1476
wordNext(bool sel)1477 void KateViewInternal::wordNext(bool sel)
1478 {
1479 WrappingCursor c(this, m_cursor);
1480
1481 // We look up into which category the current position falls:
1482 // 1. a "word" character
1483 // 2. a "non-word" character (except space)
1484 // 3. the end of the line
1485 // and skip all following characters that fall into this class.
1486 // If the skipped characters are followed by space, we skip that too.
1487 // The code assumes that space is never part of the word character class.
1488
1489 KateHighlighting *h = doc()->highlight();
1490 if (c.atEdge(right)) {
1491 ++c;
1492 } else if (h->isInWord(doc()->line(c.line())[c.column()])) {
1493 if (doc()->config()->camelCursor()) {
1494 CamelCursor cc(this, m_cursor);
1495 ++cc;
1496 updateSelection(cc, sel);
1497 updateCursor(cc);
1498 return;
1499 } else {
1500 while (!c.atEdge(right) && h->isInWord(doc()->line(c.line())[c.column()])) {
1501 ++c;
1502 }
1503 }
1504 } else {
1505 while (!c.atEdge(right)
1506 && !h->isInWord(doc()->line(c.line())[c.column()])
1507 // we must not skip space, because if that space is followed
1508 // by more non-word characters, we would skip them, too
1509 && !doc()->line(c.line())[c.column()].isSpace()) {
1510 ++c;
1511 }
1512 }
1513
1514 while (!c.atEdge(right) && doc()->line(c.line())[c.column()].isSpace()) {
1515 ++c;
1516 }
1517 updateSelection(c, sel);
1518 updateCursor(c);
1519 }
1520
moveEdge(KateViewInternal::Bias bias,bool sel)1521 void KateViewInternal::moveEdge(KateViewInternal::Bias bias, bool sel)
1522 {
1523 BoundedCursor c(this, m_cursor);
1524 c.toEdge(bias);
1525 updateSelection(c, sel);
1526 updateCursor(c);
1527 }
1528
home(bool sel)1529 void KateViewInternal::home(bool sel)
1530 {
1531 if (view()->dynWordWrap() && currentLayout().startCol()) {
1532 // Allow us to go to the real start if we're already at the start of the view line
1533 if (m_cursor.column() != currentLayout().startCol()) {
1534 KTextEditor::Cursor c = currentLayout().start();
1535 updateSelection(c, sel);
1536 updateCursor(c);
1537 return;
1538 }
1539 }
1540
1541 if (!doc()->config()->smartHome()) {
1542 moveEdge(left, sel);
1543 return;
1544 }
1545
1546 Kate::TextLine l = doc()->kateTextLine(m_cursor.line());
1547
1548 if (!l) {
1549 return;
1550 }
1551
1552 KTextEditor::Cursor c = m_cursor;
1553 int lc = l->firstChar();
1554
1555 if (lc < 0 || c.column() == lc) {
1556 c.setColumn(0);
1557 } else {
1558 c.setColumn(lc);
1559 }
1560
1561 updateSelection(c, sel);
1562 updateCursor(c, true);
1563 }
1564
end(bool sel)1565 void KateViewInternal::end(bool sel)
1566 {
1567 KateTextLayout layout = currentLayout();
1568
1569 if (view()->dynWordWrap() && layout.wrap()) {
1570 // Allow us to go to the real end if we're already at the end of the view line
1571 if (m_cursor.column() < layout.endCol() - 1) {
1572 KTextEditor::Cursor c(m_cursor.line(), layout.endCol() - 1);
1573 updateSelection(c, sel);
1574 updateCursor(c);
1575 return;
1576 }
1577 }
1578
1579 if (!doc()->config()->smartHome()) {
1580 moveEdge(right, sel);
1581 return;
1582 }
1583
1584 Kate::TextLine l = doc()->kateTextLine(m_cursor.line());
1585
1586 if (!l) {
1587 return;
1588 }
1589
1590 // "Smart End", as requested in bugs #78258 and #106970
1591 if (m_cursor.column() == doc()->lineLength(m_cursor.line())) {
1592 KTextEditor::Cursor c = m_cursor;
1593 c.setColumn(l->lastChar() + 1);
1594 updateSelection(c, sel);
1595 updateCursor(c, true);
1596 } else {
1597 moveEdge(right, sel);
1598 }
1599 }
1600
currentLayout() const1601 KateTextLayout KateViewInternal::currentLayout() const
1602 {
1603 return cache()->textLayout(m_cursor);
1604 }
1605
previousLayout() const1606 KateTextLayout KateViewInternal::previousLayout() const
1607 {
1608 int currentViewLine = cache()->viewLine(m_cursor);
1609
1610 if (currentViewLine) {
1611 return cache()->textLayout(m_cursor.line(), currentViewLine - 1);
1612 } else {
1613 return cache()->textLayout(view()->textFolding().visibleLineToLine(m_displayCursor.line() - 1), -1);
1614 }
1615 }
1616
nextLayout() const1617 KateTextLayout KateViewInternal::nextLayout() const
1618 {
1619 int currentViewLine = cache()->viewLine(m_cursor) + 1;
1620
1621 if (currentViewLine >= cache()->line(m_cursor.line())->viewLineCount()) {
1622 currentViewLine = 0;
1623 return cache()->textLayout(view()->textFolding().visibleLineToLine(m_displayCursor.line() + 1), currentViewLine);
1624 } else {
1625 return cache()->textLayout(m_cursor.line(), currentViewLine);
1626 }
1627 }
1628
1629 /*
1630 * This returns the cursor which is offset by (offset) view lines.
1631 * This is the main function which is called by code not specifically dealing with word-wrap.
1632 * The opposite conversion (cursor to offset) can be done with cache()->displayViewLine().
1633 *
1634 * The cursors involved are virtual cursors (ie. equivalent to m_displayCursor)
1635 */
1636
viewLineOffset(const KTextEditor::Cursor virtualCursor,int offset,bool keepX)1637 KTextEditor::Cursor KateViewInternal::viewLineOffset(const KTextEditor::Cursor virtualCursor, int offset, bool keepX)
1638 {
1639 if (!view()->dynWordWrap()) {
1640 KTextEditor::Cursor ret(qMin((int)view()->textFolding().visibleLines() - 1, virtualCursor.line() + offset), 0);
1641
1642 if (ret.line() < 0) {
1643 ret.setLine(0);
1644 }
1645
1646 if (keepX) {
1647 int realLine = view()->textFolding().visibleLineToLine(ret.line());
1648 KateTextLayout t = cache()->textLayout(realLine, 0);
1649 Q_ASSERT(t.isValid());
1650
1651 ret.setColumn(renderer()->xToCursor(t, m_preservedX, !view()->wrapCursor()).column());
1652 }
1653
1654 return ret;
1655 }
1656
1657 KTextEditor::Cursor realCursor = virtualCursor;
1658 realCursor.setLine(view()->textFolding().visibleLineToLine(view()->textFolding().lineToVisibleLine(virtualCursor.line())));
1659
1660 int cursorViewLine = cache()->viewLine(realCursor);
1661
1662 int currentOffset = 0;
1663 int virtualLine = 0;
1664
1665 bool forwards = (offset > 0) ? true : false;
1666
1667 if (forwards) {
1668 currentOffset = cache()->lastViewLine(realCursor.line()) - cursorViewLine;
1669 if (offset <= currentOffset) {
1670 // the answer is on the same line
1671 KateTextLayout thisLine = cache()->textLayout(realCursor.line(), cursorViewLine + offset);
1672 Q_ASSERT(thisLine.virtualLine() == (int)view()->textFolding().lineToVisibleLine(virtualCursor.line()));
1673 return KTextEditor::Cursor(virtualCursor.line(), thisLine.startCol());
1674 }
1675
1676 virtualLine = virtualCursor.line() + 1;
1677
1678 } else {
1679 offset = -offset;
1680 currentOffset = cursorViewLine;
1681 if (offset <= currentOffset) {
1682 // the answer is on the same line
1683 KateTextLayout thisLine = cache()->textLayout(realCursor.line(), cursorViewLine - offset);
1684 Q_ASSERT(thisLine.virtualLine() == (int)view()->textFolding().lineToVisibleLine(virtualCursor.line()));
1685 return KTextEditor::Cursor(virtualCursor.line(), thisLine.startCol());
1686 }
1687
1688 virtualLine = virtualCursor.line() - 1;
1689 }
1690
1691 currentOffset++;
1692
1693 while (virtualLine >= 0 && virtualLine < (int)view()->textFolding().visibleLines()) {
1694 int realLine = view()->textFolding().visibleLineToLine(virtualLine);
1695 KateLineLayoutPtr thisLine = cache()->line(realLine, virtualLine);
1696 if (!thisLine) {
1697 break;
1698 }
1699
1700 for (int i = 0; i < thisLine->viewLineCount(); ++i) {
1701 if (offset == currentOffset) {
1702 KateTextLayout thisViewLine = thisLine->viewLine(i);
1703
1704 if (!forwards) {
1705 // We actually want it the other way around
1706 int requiredViewLine = cache()->lastViewLine(realLine) - thisViewLine.viewLine();
1707 if (requiredViewLine != thisViewLine.viewLine()) {
1708 thisViewLine = thisLine->viewLine(requiredViewLine);
1709 }
1710 }
1711
1712 KTextEditor::Cursor ret(virtualLine, thisViewLine.startCol());
1713
1714 // keep column position
1715 if (keepX) {
1716 KTextEditor::Cursor realCursor = toRealCursor(virtualCursor);
1717 KateTextLayout t = cache()->textLayout(realCursor);
1718 // renderer()->cursorToX(t, realCursor, !view()->wrapCursor());
1719
1720 realCursor = renderer()->xToCursor(thisViewLine, m_preservedX, !view()->wrapCursor());
1721 ret.setColumn(realCursor.column());
1722 }
1723
1724 return ret;
1725 }
1726
1727 currentOffset++;
1728 }
1729
1730 if (forwards) {
1731 virtualLine++;
1732 } else {
1733 virtualLine--;
1734 }
1735 }
1736
1737 // Looks like we were asked for something a bit exotic.
1738 // Return the max/min valid position.
1739 if (forwards) {
1740 return KTextEditor::Cursor(view()->textFolding().visibleLines() - 1,
1741 doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1)));
1742 } else {
1743 return KTextEditor::Cursor(0, 0);
1744 }
1745 }
1746
lineMaxCursorX(const KateTextLayout & range)1747 int KateViewInternal::lineMaxCursorX(const KateTextLayout &range)
1748 {
1749 if (!view()->wrapCursor() && !range.wrap()) {
1750 return INT_MAX;
1751 }
1752
1753 int maxX = range.endX();
1754
1755 if (maxX && range.wrap()) {
1756 QChar lastCharInLine = doc()->kateTextLine(range.line())->at(range.endCol() - 1);
1757 maxX -= renderer()->currentFontMetrics().horizontalAdvance(lastCharInLine);
1758 }
1759
1760 return maxX;
1761 }
1762
lineMaxCol(const KateTextLayout & range)1763 int KateViewInternal::lineMaxCol(const KateTextLayout &range)
1764 {
1765 int maxCol = range.endCol();
1766
1767 if (maxCol && range.wrap()) {
1768 maxCol--;
1769 }
1770
1771 return maxCol;
1772 }
1773
cursorUp(bool sel)1774 void KateViewInternal::cursorUp(bool sel)
1775 {
1776 if (!sel && view()->completionWidget()->isCompletionActive()) {
1777 view()->completionWidget()->cursorUp();
1778 return;
1779 }
1780
1781 // assert that the display cursor is in visible lines
1782 Q_ASSERT(m_displayCursor.line() < view()->textFolding().visibleLines());
1783
1784 // move cursor to start of line, if we are at first line!
1785 if (m_displayCursor.line() == 0 && (!view()->dynWordWrap() || cache()->viewLine(m_cursor) == 0)) {
1786 home(sel);
1787 return;
1788 }
1789
1790 m_preserveX = true;
1791
1792 KateTextLayout thisLine = currentLayout();
1793 // This is not the first line because that is already simplified out above
1794 KateTextLayout pRange = previousLayout();
1795
1796 // Ensure we're in the right spot
1797 Q_ASSERT(m_cursor.line() == thisLine.line());
1798 Q_ASSERT(m_cursor.column() >= thisLine.startCol());
1799 Q_ASSERT(!thisLine.wrap() || m_cursor.column() < thisLine.endCol());
1800
1801 KTextEditor::Cursor c = renderer()->xToCursor(pRange, m_preservedX, !view()->wrapCursor());
1802
1803 updateSelection(c, sel);
1804 updateCursor(c);
1805 }
1806
cursorDown(bool sel)1807 void KateViewInternal::cursorDown(bool sel)
1808 {
1809 if (!sel && view()->completionWidget()->isCompletionActive()) {
1810 view()->completionWidget()->cursorDown();
1811 return;
1812 }
1813
1814 // move cursor to end of line, if we are at last line!
1815 if ((m_displayCursor.line() >= view()->textFolding().visibleLines() - 1)
1816 && (!view()->dynWordWrap() || cache()->viewLine(m_cursor) == cache()->lastViewLine(m_cursor.line()))) {
1817 end(sel);
1818 return;
1819 }
1820
1821 m_preserveX = true;
1822
1823 KateTextLayout thisLine = currentLayout();
1824 // This is not the last line because that is already simplified out above
1825 KateTextLayout nRange = nextLayout();
1826
1827 // Ensure we're in the right spot
1828 Q_ASSERT((m_cursor.line() == thisLine.line()) && (m_cursor.column() >= thisLine.startCol()) && (!thisLine.wrap() || m_cursor.column() < thisLine.endCol()));
1829
1830 KTextEditor::Cursor c = renderer()->xToCursor(nRange, m_preservedX, !view()->wrapCursor());
1831
1832 updateSelection(c, sel);
1833 updateCursor(c);
1834 }
1835
cursorToMatchingBracket(bool sel)1836 void KateViewInternal::cursorToMatchingBracket(bool sel)
1837 {
1838 KTextEditor::Cursor c = findMatchingBracket();
1839
1840 if (c.isValid()) {
1841 updateSelection(c, sel);
1842 updateCursor(c);
1843 }
1844 }
1845
topOfView(bool sel)1846 void KateViewInternal::topOfView(bool sel)
1847 {
1848 KTextEditor::Cursor c = viewLineOffset(startPos(), m_minLinesVisible);
1849 updateSelection(toRealCursor(c), sel);
1850 updateCursor(toRealCursor(c));
1851 }
1852
bottomOfView(bool sel)1853 void KateViewInternal::bottomOfView(bool sel)
1854 {
1855 KTextEditor::Cursor c = viewLineOffset(endPos(), -m_minLinesVisible);
1856 updateSelection(toRealCursor(c), sel);
1857 updateCursor(toRealCursor(c));
1858 }
1859
1860 // lines is the offset to scroll by
scrollLines(int lines,bool sel)1861 void KateViewInternal::scrollLines(int lines, bool sel)
1862 {
1863 KTextEditor::Cursor c = viewLineOffset(m_displayCursor, lines, true);
1864
1865 // Fix the virtual cursor -> real cursor
1866 c.setLine(view()->textFolding().visibleLineToLine(c.line()));
1867
1868 updateSelection(c, sel);
1869 updateCursor(c);
1870 }
1871
1872 // This is a bit misleading... it's asking for the view to be scrolled, not the cursor
scrollUp()1873 void KateViewInternal::scrollUp()
1874 {
1875 KTextEditor::Cursor newPos = viewLineOffset(startPos(), -1);
1876 scrollPos(newPos);
1877 }
1878
scrollDown()1879 void KateViewInternal::scrollDown()
1880 {
1881 KTextEditor::Cursor newPos = viewLineOffset(startPos(), 1);
1882 scrollPos(newPos);
1883 }
1884
setAutoCenterLines(int viewLines,bool updateView)1885 void KateViewInternal::setAutoCenterLines(int viewLines, bool updateView)
1886 {
1887 m_autoCenterLines = viewLines;
1888 m_minLinesVisible = qMin(int((linesDisplayed() - 1) / 2), m_autoCenterLines);
1889 if (updateView) {
1890 KateViewInternal::updateView();
1891 }
1892 }
1893
pageUp(bool sel,bool half)1894 void KateViewInternal::pageUp(bool sel, bool half)
1895 {
1896 if (view()->isCompletionActive()) {
1897 view()->completionWidget()->pageUp();
1898 return;
1899 }
1900
1901 // remember the view line and x pos
1902 int viewLine = cache()->displayViewLine(m_displayCursor);
1903 bool atTop = startPos().atStartOfDocument();
1904
1905 // Adjust for an auto-centering cursor
1906 int lineadj = m_minLinesVisible;
1907
1908 int linesToScroll;
1909 if (!half) {
1910 linesToScroll = -qMax((linesDisplayed() - 1) - lineadj, 0);
1911 } else {
1912 linesToScroll = -qMax((linesDisplayed() / 2 - 1) - lineadj, 0);
1913 }
1914
1915 m_preserveX = true;
1916
1917 if (!doc()->pageUpDownMovesCursor() && !atTop) {
1918 KTextEditor::Cursor newStartPos = viewLineOffset(startPos(), linesToScroll - 1);
1919 scrollPos(newStartPos);
1920
1921 // put the cursor back approximately where it was
1922 KTextEditor::Cursor newPos = toRealCursor(viewLineOffset(newStartPos, viewLine, true));
1923
1924 KateTextLayout newLine = cache()->textLayout(newPos);
1925
1926 newPos = renderer()->xToCursor(newLine, m_preservedX, !view()->wrapCursor());
1927
1928 m_preserveX = true;
1929 updateSelection(newPos, sel);
1930 updateCursor(newPos);
1931
1932 } else {
1933 scrollLines(linesToScroll, sel);
1934 }
1935 }
1936
pageDown(bool sel,bool half)1937 void KateViewInternal::pageDown(bool sel, bool half)
1938 {
1939 if (view()->isCompletionActive()) {
1940 view()->completionWidget()->pageDown();
1941 return;
1942 }
1943
1944 // remember the view line
1945 int viewLine = cache()->displayViewLine(m_displayCursor);
1946 bool atEnd = startPos() >= m_cachedMaxStartPos;
1947
1948 // Adjust for an auto-centering cursor
1949 int lineadj = m_minLinesVisible;
1950
1951 int linesToScroll;
1952 if (!half) {
1953 linesToScroll = qMax((linesDisplayed() - 1) - lineadj, 0);
1954 } else {
1955 linesToScroll = qMax((linesDisplayed() / 2 - 1) - lineadj, 0);
1956 }
1957
1958 m_preserveX = true;
1959
1960 if (!doc()->pageUpDownMovesCursor() && !atEnd) {
1961 KTextEditor::Cursor newStartPos = viewLineOffset(startPos(), linesToScroll + 1);
1962 scrollPos(newStartPos);
1963
1964 // put the cursor back approximately where it was
1965 KTextEditor::Cursor newPos = toRealCursor(viewLineOffset(newStartPos, viewLine, true));
1966
1967 KateTextLayout newLine = cache()->textLayout(newPos);
1968
1969 newPos = renderer()->xToCursor(newLine, m_preservedX, !view()->wrapCursor());
1970
1971 m_preserveX = true;
1972 updateSelection(newPos, sel);
1973 updateCursor(newPos);
1974
1975 } else {
1976 scrollLines(linesToScroll, sel);
1977 }
1978 }
1979
maxLen(int startLine)1980 int KateViewInternal::maxLen(int startLine)
1981 {
1982 Q_ASSERT(!view()->dynWordWrap());
1983
1984 int displayLines = (view()->height() / renderer()->lineHeight()) + 1;
1985
1986 int maxLen = 0;
1987
1988 for (int z = 0; z < displayLines; z++) {
1989 int virtualLine = startLine + z;
1990
1991 if (virtualLine < 0 || virtualLine >= (int)view()->textFolding().visibleLines()) {
1992 break;
1993 }
1994
1995 maxLen = qMax(maxLen, cache()->line(view()->textFolding().visibleLineToLine(virtualLine))->width());
1996 }
1997
1998 return maxLen;
1999 }
2000
columnScrollingPossible()2001 bool KateViewInternal::columnScrollingPossible()
2002 {
2003 return !view()->dynWordWrap() && m_columnScroll->isEnabled() && (m_columnScroll->maximum() > 0);
2004 }
2005
lineScrollingPossible()2006 bool KateViewInternal::lineScrollingPossible()
2007 {
2008 return m_lineScroll->minimum() != m_lineScroll->maximum();
2009 }
2010
top(bool sel)2011 void KateViewInternal::top(bool sel)
2012 {
2013 KTextEditor::Cursor newCursor(0, 0);
2014
2015 newCursor = renderer()->xToCursor(cache()->textLayout(newCursor), m_preservedX, !view()->wrapCursor());
2016
2017 updateSelection(newCursor, sel);
2018 updateCursor(newCursor);
2019 }
2020
bottom(bool sel)2021 void KateViewInternal::bottom(bool sel)
2022 {
2023 KTextEditor::Cursor newCursor(doc()->lastLine(), 0);
2024
2025 newCursor = renderer()->xToCursor(cache()->textLayout(newCursor), m_preservedX, !view()->wrapCursor());
2026
2027 updateSelection(newCursor, sel);
2028 updateCursor(newCursor);
2029 }
2030
top_home(bool sel)2031 void KateViewInternal::top_home(bool sel)
2032 {
2033 if (view()->isCompletionActive()) {
2034 view()->completionWidget()->top();
2035 return;
2036 }
2037
2038 KTextEditor::Cursor c(0, 0);
2039 updateSelection(c, sel);
2040 updateCursor(c);
2041 }
2042
bottom_end(bool sel)2043 void KateViewInternal::bottom_end(bool sel)
2044 {
2045 if (view()->isCompletionActive()) {
2046 view()->completionWidget()->bottom();
2047 return;
2048 }
2049
2050 KTextEditor::Cursor c(doc()->lastLine(), doc()->lineLength(doc()->lastLine()));
2051 updateSelection(c, sel);
2052 updateCursor(c);
2053 }
2054
updateSelection(const KTextEditor::Cursor _newCursor,bool keepSel)2055 void KateViewInternal::updateSelection(const KTextEditor::Cursor _newCursor, bool keepSel)
2056 {
2057 KTextEditor::Cursor newCursor = _newCursor;
2058 if (keepSel) {
2059 if (!view()->selection()
2060 || (m_selectAnchor.line() == -1)
2061 // don't kill the selection if we have a persistent selection and
2062 // the cursor is inside or at the boundaries of the selected area
2063 || (view()->config()->persistentSelection()
2064 && !(view()->selectionRange().contains(m_cursor) || view()->selectionRange().boundaryAtCursor(m_cursor)))) {
2065 m_selectAnchor = m_cursor;
2066 setSelection(KTextEditor::Range(m_cursor, newCursor));
2067 } else {
2068 bool doSelect = true;
2069 switch (m_selectionMode) {
2070 case Word: {
2071 // Restore selStartCached if needed. It gets nuked by
2072 // viewSelectionChanged if we drag the selection into non-existence,
2073 // which can legitimately happen if a shift+DC selection is unable to
2074 // set a "proper" (i.e. non-empty) cached selection, e.g. because the
2075 // start was on something that isn't a word. Word select mode relies
2076 // on the cached selection being set properly, even if it is empty
2077 // (i.e. selStartCached == selEndCached).
2078 if (!m_selectionCached.isValid()) {
2079 m_selectionCached.setStart(m_selectionCached.end());
2080 }
2081
2082 int c;
2083 if (newCursor > m_selectionCached.start()) {
2084 m_selectAnchor = m_selectionCached.start();
2085
2086 Kate::TextLine l = doc()->kateTextLine(newCursor.line());
2087
2088 c = newCursor.column();
2089 if (c > 0 && doc()->highlight()->isInWord(l->at(c - 1))) {
2090 for (; c < l->length(); c++) {
2091 if (!doc()->highlight()->isInWord(l->at(c))) {
2092 break;
2093 }
2094 }
2095 }
2096
2097 newCursor.setColumn(c);
2098 } else if (newCursor < m_selectionCached.start()) {
2099 m_selectAnchor = m_selectionCached.end();
2100
2101 Kate::TextLine l = doc()->kateTextLine(newCursor.line());
2102
2103 c = newCursor.column();
2104 if (c > 0 && c < doc()->lineLength(newCursor.line()) && doc()->highlight()->isInWord(l->at(c))
2105 && doc()->highlight()->isInWord(l->at(c - 1))) {
2106 for (c -= 2; c >= 0; c--) {
2107 if (!doc()->highlight()->isInWord(l->at(c))) {
2108 break;
2109 }
2110 }
2111 newCursor.setColumn(c + 1);
2112 }
2113 } else {
2114 doSelect = false;
2115 }
2116
2117 } break;
2118 case Line:
2119 if (!m_selectionCached.isValid()) {
2120 m_selectionCached = KTextEditor::Range(endLine(), 0, endLine(), 0);
2121 }
2122 if (newCursor.line() > m_selectionCached.start().line()) {
2123 if (newCursor.line() + 1 >= doc()->lines()) {
2124 newCursor.setColumn(doc()->line(newCursor.line()).length());
2125 } else {
2126 newCursor.setPosition(newCursor.line() + 1, 0);
2127 }
2128 // Grow to include the entire line
2129 m_selectAnchor = m_selectionCached.start();
2130 m_selectAnchor.setColumn(0);
2131 } else if (newCursor.line() < m_selectionCached.start().line()) {
2132 newCursor.setColumn(0);
2133 // Grow to include entire line
2134 m_selectAnchor = m_selectionCached.end();
2135 if (m_selectAnchor.column() > 0) {
2136 if (m_selectAnchor.line() + 1 >= doc()->lines()) {
2137 m_selectAnchor.setColumn(doc()->line(newCursor.line()).length());
2138 } else {
2139 m_selectAnchor.setPosition(m_selectAnchor.line() + 1, 0);
2140 }
2141 }
2142 } else { // same line, ignore
2143 doSelect = false;
2144 }
2145 break;
2146 case Mouse: {
2147 if (!m_selectionCached.isValid()) {
2148 break;
2149 }
2150
2151 if (newCursor > m_selectionCached.end()) {
2152 m_selectAnchor = m_selectionCached.start();
2153 } else if (newCursor < m_selectionCached.start()) {
2154 m_selectAnchor = m_selectionCached.end();
2155 } else {
2156 doSelect = false;
2157 }
2158 } break;
2159 default: /* nothing special to do */;
2160 }
2161
2162 if (doSelect) {
2163 setSelection(KTextEditor::Range(m_selectAnchor, newCursor));
2164 } else if (m_selectionCached.isValid()) { // we have a cached selection, so we restore that
2165 setSelection(m_selectionCached);
2166 }
2167 }
2168
2169 m_selChangedByUser = true;
2170 } else if (!view()->config()->persistentSelection()) {
2171 view()->clearSelection();
2172
2173 m_selectionCached = KTextEditor::Range::invalid();
2174 m_selectAnchor = KTextEditor::Cursor::invalid();
2175 }
2176
2177 #ifndef QT_NO_ACCESSIBILITY
2178 // FIXME KF5
2179 // QAccessibleTextSelectionEvent ev(this, /* selection start, selection end*/);
2180 // QAccessible::updateAccessibility(&ev);
2181 #endif
2182 }
2183
setSelection(const KTextEditor::Range & range)2184 void KateViewInternal::setSelection(const KTextEditor::Range &range)
2185 {
2186 disconnect(m_view, &KTextEditor::ViewPrivate::selectionChanged, this, &KateViewInternal::viewSelectionChanged);
2187 view()->setSelection(range);
2188 connect(m_view, &KTextEditor::ViewPrivate::selectionChanged, this, &KateViewInternal::viewSelectionChanged);
2189 }
2190
moveCursorToSelectionEdge()2191 void KateViewInternal::moveCursorToSelectionEdge()
2192 {
2193 if (!view()->selection()) {
2194 return;
2195 }
2196
2197 int tmp = m_minLinesVisible;
2198 m_minLinesVisible = 0;
2199
2200 if (view()->selectionRange().start() < m_selectAnchor) {
2201 updateCursor(view()->selectionRange().start());
2202 } else {
2203 updateCursor(view()->selectionRange().end());
2204 }
2205
2206 m_minLinesVisible = tmp;
2207 }
2208
findMatchingFoldingMarker(const KTextEditor::Cursor currentCursorPos,const int value,const int maxLines)2209 KTextEditor::Range KateViewInternal::findMatchingFoldingMarker(const KTextEditor::Cursor currentCursorPos, const int value, const int maxLines)
2210 {
2211 const int direction = !(value < 0) ? 1 : -1;
2212 int foldCounter = 0;
2213 int lineCounter = 0;
2214 auto &foldMarkers = m_view->doc()->buffer().plainLine(currentCursorPos.line())->foldings();
2215
2216 // searching a end folding marker? go left to right
2217 // otherwise, go right to left
2218 long i = direction == 1 ? 0 : (long)foldMarkers.size() - 1;
2219
2220 // For the first line, we start considering the first folding after the cursor
2221 for (; i >= 0 && i < (long)foldMarkers.size(); i += direction) {
2222 if ((foldMarkers[i].offset - currentCursorPos.column()) * direction > 0) {
2223 if (foldMarkers[i].foldingValue == value) {
2224 foldCounter += 1;
2225 } else if (foldMarkers[i].foldingValue == -value && foldCounter > 0) {
2226 foldCounter -= 1;
2227 } else if (foldMarkers[i].foldingValue == -value && foldCounter == 0) {
2228 return KTextEditor::Range(currentCursorPos.line(),
2229 getStartOffset(direction, foldMarkers[i].offset, foldMarkers[i].length),
2230 currentCursorPos.line(),
2231 getEndOffset(direction, foldMarkers[i].offset, foldMarkers[i].length));
2232 }
2233 }
2234 }
2235
2236 // for the other lines
2237 int currentLine = currentCursorPos.line() + direction;
2238 for (; currentLine >= 0 && currentLine < m_view->doc()->lines() && lineCounter < maxLines; currentLine += direction) {
2239 // update line attributes
2240 auto &foldMarkers = m_view->doc()->buffer().plainLine(currentLine)->foldings();
2241 i = direction == 1 ? 0 : (long)foldMarkers.size() - 1;
2242
2243 // iterate through the markers
2244 for (; i >= 0 && i < (long)foldMarkers.size(); i += direction) {
2245 if (foldMarkers[i].foldingValue == value) {
2246 foldCounter += 1;
2247 } else if (foldMarkers[i].foldingValue == -value && foldCounter != 0) {
2248 foldCounter -= 1;
2249 } else if (foldMarkers[i].foldingValue == -value && foldCounter == 0) {
2250 return KTextEditor::Range(currentLine,
2251 getStartOffset(direction, foldMarkers[i].offset, foldMarkers[i].length),
2252 currentLine,
2253 getEndOffset(direction, foldMarkers[i].offset, foldMarkers[i].length));
2254 }
2255 }
2256 lineCounter += 1;
2257 }
2258
2259 // got out of loop, no matching folding found
2260 // returns a invalid folding range
2261 return KTextEditor::Range::invalid();
2262 }
2263
updateFoldingMarkersHighlighting()2264 void KateViewInternal::updateFoldingMarkersHighlighting()
2265 {
2266 auto &foldings = m_view->doc()->buffer().plainLine(m_cursor.line())->foldings();
2267
2268 for (unsigned long i = 0; i < foldings.size(); i++) {
2269 // 1 -> left to right, the current folding is start type
2270 // -1 -> right to left, the current folding is end type
2271 int direction = !(foldings[i].foldingValue < 0) ? 1 : -1;
2272
2273 int startOffset = getStartOffset(-direction, foldings[i].offset, foldings[i].length);
2274 int endOffset = getEndOffset(-direction, foldings[i].offset, foldings[i].length);
2275
2276 if (m_cursor.column() >= startOffset && m_cursor.column() <= endOffset) {
2277 const auto foldingMarkerMatch = findMatchingFoldingMarker(KTextEditor::Cursor(m_cursor.line(), m_cursor.column()), foldings[i].foldingValue, 2000);
2278
2279 if (!foldingMarkerMatch.isValid()) {
2280 break;
2281 }
2282
2283 // set fmStart to Opening Folding Marker and fmEnd to Ending Folding Marker
2284 if (direction == 1) {
2285 m_fmStart->setRange(KTextEditor::Range(m_cursor.line(), startOffset, m_cursor.line(), endOffset));
2286 m_fmEnd->setRange(foldingMarkerMatch);
2287 } else {
2288 m_fmStart->setRange(foldingMarkerMatch);
2289 m_fmEnd->setRange(KTextEditor::Range(m_cursor.line(), startOffset, m_cursor.line(), endOffset));
2290 }
2291
2292 KTextEditor::Attribute::Ptr fill = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute());
2293 fill->setBackground(view()->m_renderer->config()->highlightedBracketColor());
2294
2295 m_fmStart->setAttribute(fill);
2296 m_fmEnd->setAttribute(fill);
2297 return;
2298 }
2299 }
2300 m_fmStart->setRange(KTextEditor::Range::invalid());
2301 m_fmEnd->setRange(KTextEditor::Range::invalid());
2302 }
2303
updateCursor(const KTextEditor::Cursor newCursor,bool force,bool center,bool calledExternally)2304 void KateViewInternal::updateCursor(const KTextEditor::Cursor newCursor, bool force, bool center, bool calledExternally)
2305 {
2306 if (!force && (m_cursor.toCursor() == newCursor)) {
2307 m_displayCursor = toVirtualCursor(newCursor);
2308 if (!m_madeVisible && m_view == doc()->activeView()) {
2309 // unfold if required
2310 view()->textFolding().ensureLineIsVisible(newCursor.line());
2311
2312 makeVisible(m_displayCursor, m_displayCursor.column(), false, center, calledExternally);
2313 }
2314
2315 return;
2316 }
2317
2318 if (m_cursor.line() != newCursor.line()) {
2319 m_leftBorder->updateForCursorLineChange();
2320 }
2321
2322 // unfold if required
2323 view()->textFolding().ensureLineIsVisible(newCursor.line());
2324
2325 KTextEditor::Cursor oldDisplayCursor = m_displayCursor;
2326
2327 m_displayCursor = toVirtualCursor(newCursor);
2328 m_cursor.setPosition(newCursor);
2329
2330 if (m_view == doc()->activeView()) {
2331 makeVisible(m_displayCursor, m_displayCursor.column(), false, center, calledExternally);
2332 }
2333
2334 updateBracketMarks();
2335
2336 updateFoldingMarkersHighlighting();
2337
2338 // avoid double work, tagLine => tagLines => not that cheap, much more costly than to compare 2 ints
2339 tagLine(oldDisplayCursor);
2340 if (oldDisplayCursor.line() != m_displayCursor.line()) {
2341 tagLine(m_displayCursor);
2342 }
2343
2344 updateMicroFocus();
2345
2346 if (m_cursorTimer.isActive()) {
2347 if (QApplication::cursorFlashTime() > 0) {
2348 m_cursorTimer.start(QApplication::cursorFlashTime() / 2);
2349 }
2350 renderer()->setDrawCaret(true);
2351 }
2352
2353 // Remember the maximum X position if requested
2354 if (m_preserveX) {
2355 m_preserveX = false;
2356 } else {
2357 m_preservedX = renderer()->cursorToX(cache()->textLayout(m_cursor), m_cursor, !view()->wrapCursor());
2358 }
2359
2360 // qCDebug(LOG_KTE) << "m_preservedX: " << m_preservedX << " (was "<< oldmaxx << "), m_cursorX: " << m_cursorX;
2361 // qCDebug(LOG_KTE) << "Cursor now located at real " << cursor.line << "," << cursor.col << ", virtual " << m_displayCursor.line << ", " <<
2362 // m_displayCursor.col << "; Top is " << startLine() << ", " << startPos().col;
2363
2364 cursorMoved();
2365
2366 updateDirty(); // paintText(0, 0, width(), height(), true);
2367
2368 Q_EMIT view()->cursorPositionChanged(m_view, m_cursor);
2369 }
2370
updateBracketMarkAttributes()2371 void KateViewInternal::updateBracketMarkAttributes()
2372 {
2373 KTextEditor::Attribute::Ptr bracketFill = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute());
2374 bracketFill->setBackground(view()->m_renderer->config()->highlightedBracketColor());
2375 bracketFill->setBackgroundFillWhitespace(false);
2376 if (QFontInfo(renderer()->currentFont()).fixedPitch()) {
2377 // make font bold only for fixed fonts, otherwise text jumps around
2378 bracketFill->setFontBold();
2379 }
2380
2381 m_bmStart->setAttribute(bracketFill);
2382 m_bmEnd->setAttribute(bracketFill);
2383
2384 if (view()->m_renderer->config()->showWholeBracketExpression()) {
2385 KTextEditor::Attribute::Ptr expressionFill = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute());
2386 expressionFill->setBackground(view()->m_renderer->config()->highlightedBracketColor());
2387 expressionFill->setBackgroundFillWhitespace(false);
2388
2389 m_bm->setAttribute(expressionFill);
2390 } else {
2391 m_bm->setAttribute(KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()));
2392 }
2393 }
2394
updateBracketMarks()2395 void KateViewInternal::updateBracketMarks()
2396 {
2397 // add some limit to this, this is really endless on big files without limit
2398 const int maxLines = 5000;
2399 const KTextEditor::Range newRange = doc()->findMatchingBracket(m_cursor, maxLines);
2400
2401 // new range valid, then set ranges to it
2402 if (newRange.isValid()) {
2403 if (m_bm->toRange() == newRange) {
2404 // hide preview as it now (probably) blocks the top of the view
2405 hideBracketMatchPreview();
2406 return;
2407 }
2408
2409 // modify full range
2410 m_bm->setRange(newRange);
2411
2412 // modify start and end ranges
2413 m_bmStart->setRange(KTextEditor::Range(m_bm->start(), KTextEditor::Cursor(m_bm->start().line(), m_bm->start().column() + 1)));
2414 m_bmEnd->setRange(KTextEditor::Range(m_bm->end(), KTextEditor::Cursor(m_bm->end().line(), m_bm->end().column() + 1)));
2415
2416 // show preview of the matching bracket's line
2417 if (m_view->config()->value(KateViewConfig::ShowBracketMatchPreview).toBool()) {
2418 showBracketMatchPreview();
2419 }
2420
2421 // flash matching bracket
2422 if (!renderer()->config()->animateBracketMatching()) {
2423 return;
2424 }
2425
2426 const KTextEditor::Cursor flashPos = (m_cursor == m_bmStart->start() || m_cursor == m_bmStart->end()) ? m_bmEnd->start() : m_bm->start();
2427 if (flashPos != m_bmLastFlashPos->toCursor()) {
2428 m_bmLastFlashPos->setPosition(flashPos);
2429
2430 KTextEditor::Attribute::Ptr attribute = attributeAt(flashPos);
2431 attribute->setBackground(view()->m_renderer->config()->highlightedBracketColor());
2432 attribute->setFontBold(m_bmStart->attribute()->fontBold());
2433
2434 flashChar(flashPos, attribute);
2435 }
2436 return;
2437 }
2438
2439 // new range was invalid
2440 m_bm->setRange(KTextEditor::Range::invalid());
2441 m_bmStart->setRange(KTextEditor::Range::invalid());
2442 m_bmEnd->setRange(KTextEditor::Range::invalid());
2443 m_bmLastFlashPos->setPosition(KTextEditor::Cursor::invalid());
2444 hideBracketMatchPreview();
2445 }
2446
tagLine(const KTextEditor::Cursor virtualCursor)2447 bool KateViewInternal::tagLine(const KTextEditor::Cursor virtualCursor)
2448 {
2449 // we had here some special case handling for one line, it was just randomly wrong for dyn. word wrapped stuff => use the generic function
2450 return tagLines(virtualCursor, virtualCursor, false);
2451 }
2452
tagLines(int start,int end,bool realLines)2453 bool KateViewInternal::tagLines(int start, int end, bool realLines)
2454 {
2455 return tagLines(KTextEditor::Cursor(start, 0), KTextEditor::Cursor(end, -1), realLines);
2456 }
2457
tagLines(KTextEditor::Cursor start,KTextEditor::Cursor end,bool realCursors)2458 bool KateViewInternal::tagLines(KTextEditor::Cursor start, KTextEditor::Cursor end, bool realCursors)
2459 {
2460 if (realCursors) {
2461 cache()->relayoutLines(start.line(), end.line());
2462
2463 // qCDebug(LOG_KTE)<<"realLines is true";
2464 start = toVirtualCursor(start);
2465 end = toVirtualCursor(end);
2466
2467 } else {
2468 cache()->relayoutLines(toRealCursor(start).line(), toRealCursor(end).line());
2469 }
2470
2471 if (end.line() < startLine()) {
2472 // qCDebug(LOG_KTE)<<"end<startLine";
2473 return false;
2474 }
2475 // Used to be > endLine(), but cache may not be valid when checking, so use a
2476 // less optimal but still adequate approximation (potential overestimation but minimal performance difference)
2477 if (start.line() > startLine() + cache()->viewCacheLineCount()) {
2478 // qCDebug(LOG_KTE)<<"start> endLine"<<start<<" "<<(endLine());
2479 return false;
2480 }
2481
2482 cache()->updateViewCache(startPos());
2483
2484 // qCDebug(LOG_KTE) << "tagLines( [" << start << "], [" << end << "] )";
2485
2486 bool ret = false;
2487
2488 for (int z = 0; z < cache()->viewCacheLineCount(); z++) {
2489 KateTextLayout &line = cache()->viewLine(z);
2490 if ((line.virtualLine() > start.line() || (line.virtualLine() == start.line() && line.endCol() >= start.column() && start.column() != -1))
2491 && (line.virtualLine() < end.line() || (line.virtualLine() == end.line() && (line.startCol() <= end.column() || end.column() == -1)))) {
2492 ret = true;
2493 break;
2494 // qCDebug(LOG_KTE) << "Tagged line " << line.line();
2495 }
2496 }
2497
2498 if (!view()->dynWordWrap()) {
2499 int y = lineToY(start.line());
2500 // FIXME is this enough for when multiple lines are deleted
2501 int h = (end.line() - start.line() + 2) * renderer()->lineHeight();
2502 if (end.line() >= view()->textFolding().visibleLines() - 1) {
2503 h = height();
2504 }
2505
2506 m_leftBorder->update(0, y, m_leftBorder->width(), h);
2507 } else {
2508 // FIXME Do we get enough good info in editRemoveText to optimize this more?
2509 // bool justTagged = false;
2510 for (int z = 0; z < cache()->viewCacheLineCount(); z++) {
2511 KateTextLayout &line = cache()->viewLine(z);
2512 if (!line.isValid()
2513 || ((line.virtualLine() > start.line() || (line.virtualLine() == start.line() && line.endCol() >= start.column() && start.column() != -1))
2514 && (line.virtualLine() < end.line() || (line.virtualLine() == end.line() && (line.startCol() <= end.column() || end.column() == -1))))) {
2515 // justTagged = true;
2516 m_leftBorder->update(0, z * renderer()->lineHeight(), m_leftBorder->width(), m_leftBorder->height());
2517 break;
2518 }
2519 /*else if (justTagged)
2520 {
2521 justTagged = false;
2522 leftBorder->update (0, z * doc()->viewFont.fontHeight, leftBorder->width(), doc()->viewFont.fontHeight);
2523 break;
2524 }*/
2525 }
2526 }
2527
2528 return ret;
2529 }
2530
tagRange(const KTextEditor::Range & range,bool realCursors)2531 bool KateViewInternal::tagRange(const KTextEditor::Range &range, bool realCursors)
2532 {
2533 return tagLines(range.start(), range.end(), realCursors);
2534 }
2535
tagAll()2536 void KateViewInternal::tagAll()
2537 {
2538 // clear the cache...
2539 cache()->clear();
2540
2541 m_leftBorder->updateFont();
2542 m_leftBorder->update();
2543 }
2544
paintCursor()2545 void KateViewInternal::paintCursor()
2546 {
2547 if (tagLine(m_displayCursor)) {
2548 updateDirty(); // paintText (0,0,width(), height(), true);
2549 }
2550 }
2551
2552 // Point in content coordinates
placeCursor(const QPoint & p,bool keepSelection,bool updateSelection)2553 void KateViewInternal::placeCursor(const QPoint &p, bool keepSelection, bool updateSelection)
2554 {
2555 KateTextLayout thisLine = yToKateTextLayout(p.y());
2556 KTextEditor::Cursor c;
2557
2558 if (!thisLine.isValid()) { // probably user clicked below the last line -> use the last line
2559 thisLine = cache()->textLayout(doc()->lines() - 1, -1);
2560 }
2561
2562 c = renderer()->xToCursor(thisLine, startX() + p.x(), !view()->wrapCursor());
2563
2564 if (c.line() < 0 || c.line() >= doc()->lines()) {
2565 return;
2566 }
2567
2568 if (updateSelection) {
2569 KateViewInternal::updateSelection(c, keepSelection);
2570 }
2571
2572 int tmp = m_minLinesVisible;
2573 m_minLinesVisible = 0;
2574 updateCursor(c);
2575 m_minLinesVisible = tmp;
2576
2577 if (updateSelection && keepSelection) {
2578 moveCursorToSelectionEdge();
2579 }
2580 }
2581
2582 // Point in content coordinates
isTargetSelected(const QPoint & p)2583 bool KateViewInternal::isTargetSelected(const QPoint &p)
2584 {
2585 const KateTextLayout &thisLine = yToKateTextLayout(p.y());
2586 if (!thisLine.isValid()) {
2587 return false;
2588 }
2589
2590 return view()->cursorSelected(renderer()->xToCursor(thisLine, startX() + p.x(), !view()->wrapCursor()));
2591 }
2592
2593 // BEGIN EVENT HANDLING STUFF
2594
eventFilter(QObject * obj,QEvent * e)2595 bool KateViewInternal::eventFilter(QObject *obj, QEvent *e)
2596 {
2597 switch (e->type()) {
2598 case QEvent::ChildAdded:
2599 case QEvent::ChildRemoved: {
2600 QChildEvent *c = static_cast<QChildEvent *>(e);
2601 if (c->added()) {
2602 c->child()->installEventFilter(this);
2603
2604 } else if (c->removed()) {
2605 c->child()->removeEventFilter(this);
2606 }
2607 } break;
2608
2609 case QEvent::ShortcutOverride: {
2610 QKeyEvent *k = static_cast<QKeyEvent *>(e);
2611
2612 if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) {
2613 if (view()->isCompletionActive()) {
2614 view()->abortCompletion();
2615 k->accept();
2616 // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "aborting completion";
2617 return true;
2618 } else if (!view()->bottomViewBar()->hiddenOrPermanent()) {
2619 view()->bottomViewBar()->hideCurrentBarWidget();
2620 k->accept();
2621 // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "closing view bar";
2622 return true;
2623 } else if (!view()->config()->persistentSelection() && view()->selection()) {
2624 m_currentInputMode->clearSelection();
2625 k->accept();
2626 // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "clearing selection";
2627 return true;
2628 }
2629 }
2630
2631 if (m_currentInputMode->stealKey(k)) {
2632 k->accept();
2633 return true;
2634 }
2635
2636 // CompletionReplayer.replay only gets called when a Ctrl-Space gets to InsertViMode::handleKeyPress
2637 // Workaround for BUG: 334032 (https://bugs.kde.org/show_bug.cgi?id=334032)
2638 if (k->key() == Qt::Key_Space && k->modifiers() == Qt::ControlModifier) {
2639 keyPressEvent(k);
2640 if (k->isAccepted()) {
2641 return true;
2642 }
2643 }
2644
2645 } break;
2646
2647 case QEvent::KeyPress: {
2648 QKeyEvent *k = static_cast<QKeyEvent *>(e);
2649
2650 // Override all other single key shortcuts which do not use a modifier other than Shift
2651 if (obj == this && (!k->modifiers() || k->modifiers() == Qt::ShiftModifier)) {
2652 keyPressEvent(k);
2653 if (k->isAccepted()) {
2654 // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "using keystroke";
2655 return true;
2656 }
2657 }
2658
2659 // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "ignoring";
2660 } break;
2661
2662 case QEvent::DragMove: {
2663 QPoint currentPoint = ((QDragMoveEvent *)e)->pos();
2664
2665 QRect doNotScrollRegion(s_scrollMargin, s_scrollMargin, width() - s_scrollMargin * 2, height() - s_scrollMargin * 2);
2666
2667 if (!doNotScrollRegion.contains(currentPoint)) {
2668 startDragScroll();
2669 // Keep sending move events
2670 ((QDragMoveEvent *)e)->accept(QRect(0, 0, 0, 0));
2671 }
2672
2673 dragMoveEvent((QDragMoveEvent *)e);
2674 } break;
2675
2676 case QEvent::DragLeave:
2677 // happens only when pressing ESC while dragging
2678 stopDragScroll();
2679 break;
2680
2681 case QEvent::WindowDeactivate:
2682 hideBracketMatchPreview();
2683 break;
2684
2685 case QEvent::ScrollPrepare: {
2686 QScrollPrepareEvent *s = static_cast<QScrollPrepareEvent *>(e);
2687 scrollPrepareEvent(s);
2688 } return true;
2689
2690 case QEvent::Scroll: {
2691 QScrollEvent *s = static_cast<QScrollEvent *>(e);
2692 scrollEvent(s);
2693 } return true;
2694
2695 default:
2696 break;
2697 }
2698
2699 return QWidget::eventFilter(obj, e);
2700 }
2701
keyPressEvent(QKeyEvent * e)2702 void KateViewInternal::keyPressEvent(QKeyEvent *e)
2703 {
2704 m_shiftKeyPressed = e->modifiers() & Qt::ShiftModifier;
2705 if (e->key() == Qt::Key_Left && e->modifiers() == Qt::AltModifier) {
2706 view()->emitNavigateLeft();
2707 e->setAccepted(true);
2708 return;
2709 }
2710 if (e->key() == Qt::Key_Right && e->modifiers() == Qt::AltModifier) {
2711 view()->emitNavigateRight();
2712 e->setAccepted(true);
2713 return;
2714 }
2715 if (e->key() == Qt::Key_Up && e->modifiers() == Qt::AltModifier) {
2716 view()->emitNavigateUp();
2717 e->setAccepted(true);
2718 return;
2719 }
2720 if (e->key() == Qt::Key_Down && e->modifiers() == Qt::AltModifier) {
2721 view()->emitNavigateDown();
2722 e->setAccepted(true);
2723 return;
2724 }
2725 if (e->key() == Qt::Key_Return && e->modifiers() == Qt::AltModifier) {
2726 view()->emitNavigateAccept();
2727 e->setAccepted(true);
2728 return;
2729 }
2730 if (e->key() == Qt::Key_Backspace && e->modifiers() == Qt::AltModifier) {
2731 view()->emitNavigateBack();
2732 e->setAccepted(true);
2733 return;
2734 }
2735
2736 if (e->key() == Qt::Key_Alt && view()->completionWidget()->isCompletionActive()) {
2737 m_completionItemExpanded = view()->completionWidget()->toggleExpanded(true);
2738 view()->completionWidget()->resetHadNavigation();
2739 m_altDownTime.start();
2740 }
2741
2742 // Note: AND'ing with <Shift> is a quick hack to fix Key_Enter
2743 const int key = e->key() | (e->modifiers() & Qt::ShiftModifier);
2744
2745 if (m_currentInputMode->keyPress(e)) {
2746 return;
2747 }
2748
2749 if (!doc()->isReadWrite()) {
2750 e->ignore();
2751 return;
2752 }
2753
2754 if ((key == Qt::Key_Return) || (key == Qt::Key_Enter) || (key == Qt::SHIFT + Qt::Key_Return) || (key == Qt::SHIFT + Qt::Key_Enter)) {
2755 view()->keyReturn();
2756 e->accept();
2757 return;
2758 }
2759
2760 if (key == Qt::Key_Backspace || key == Qt::SHIFT + Qt::Key_Backspace) {
2761 // view()->backspace();
2762 e->accept();
2763
2764 return;
2765 }
2766
2767 if (key == Qt::Key_Tab || key == Qt::SHIFT + Qt::Key_Backtab || key == Qt::Key_Backtab) {
2768 if (view()->completionWidget()->isCompletionActive()) {
2769 e->accept();
2770 view()->completionWidget()->tab(key != Qt::Key_Tab);
2771 return;
2772 }
2773
2774 if (key == Qt::Key_Tab) {
2775 uint tabHandling = doc()->config()->tabHandling();
2776 // convert tabSmart into tabInsertsTab or tabIndents:
2777 if (tabHandling == KateDocumentConfig::tabSmart) {
2778 // multiple lines selected
2779 if (view()->selection() && !view()->selectionRange().onSingleLine()) {
2780 tabHandling = KateDocumentConfig::tabIndents;
2781 }
2782
2783 // otherwise: take look at cursor position
2784 else {
2785 // if the cursor is at or before the first non-space character
2786 // or on an empty line,
2787 // Tab indents, otherwise it inserts a tab character.
2788 Kate::TextLine line = doc()->kateTextLine(m_cursor.line());
2789 int first = line->firstChar();
2790 if (first < 0 || m_cursor.column() <= first) {
2791 tabHandling = KateDocumentConfig::tabIndents;
2792 } else {
2793 tabHandling = KateDocumentConfig::tabInsertsTab;
2794 }
2795 }
2796 }
2797
2798 // either we just insert a tab or we convert that into an indent action
2799 if (tabHandling == KateDocumentConfig::tabInsertsTab) {
2800 doc()->typeChars(m_view, QStringLiteral("\t"));
2801 } else {
2802 doc()->indent(view()->selection() ? view()->selectionRange() : KTextEditor::Range(m_cursor.line(), 0, m_cursor.line(), 0), 1);
2803 }
2804
2805 e->accept();
2806
2807 return;
2808 } else if (doc()->config()->tabHandling() != KateDocumentConfig::tabInsertsTab) {
2809 // key == Qt::SHIFT+Qt::Key_Backtab || key == Qt::Key_Backtab
2810 doc()->indent(view()->selection() ? view()->selectionRange() : KTextEditor::Range(m_cursor.line(), 0, m_cursor.line(), 0), -1);
2811 e->accept();
2812
2813 return;
2814 }
2815 }
2816
2817 if (isAcceptableInput(e)) {
2818 doc()->typeChars(m_view, e->text());
2819 e->accept();
2820 return;
2821 }
2822
2823 e->ignore();
2824 }
2825
keyReleaseEvent(QKeyEvent * e)2826 void KateViewInternal::keyReleaseEvent(QKeyEvent *e)
2827 {
2828 if (e->key() == Qt::Key_Alt && view()->completionWidget()->isCompletionActive()
2829 && ((m_completionItemExpanded && (view()->completionWidget()->hadNavigation() || m_altDownTime.elapsed() > 300))
2830 || (!m_completionItemExpanded && !view()->completionWidget()->hadNavigation()))) {
2831 view()->completionWidget()->toggleExpanded(false, true);
2832 }
2833
2834 if (m_shiftKeyPressed && (e->modifiers() & Qt::ShiftModifier) == 0) {
2835 m_shiftKeyPressed = false;
2836
2837 if (m_selChangedByUser) {
2838 if (view()->selection()) {
2839 QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection);
2840 }
2841
2842 m_selChangedByUser = false;
2843 }
2844 }
2845
2846 e->ignore();
2847 return;
2848 }
2849
isAcceptableInput(const QKeyEvent * e) const2850 bool KateViewInternal::isAcceptableInput(const QKeyEvent *e) const
2851 {
2852 // reimplemented from QInputControl::isAcceptableInput()
2853
2854 const QString text = e->text();
2855 if (text.isEmpty()) {
2856 return false;
2857 }
2858
2859 const QChar c = text.at(0);
2860
2861 // Formatting characters such as ZWNJ, ZWJ, RLM, etc. This needs to go before the
2862 // next test, since CTRL+SHIFT is sometimes used to input it on Windows.
2863 // see bug 389796 (typing formatting characters such as ZWNJ)
2864 // and bug 396764 (typing soft-hyphens)
2865 if (c.category() == QChar::Other_Format) {
2866 return true;
2867 }
2868
2869 // QTBUG-35734: ignore Ctrl/Ctrl+Shift; accept only AltGr (Alt+Ctrl) on German keyboards
2870 if ((e->modifiers() == Qt::ControlModifier) || (e->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier))) {
2871 return false;
2872 }
2873
2874 // printable or private use is good, see e.g. bug 366424 (typing "private use" unicode characters)
2875 return c.isPrint() || (c.category() == QChar::Other_PrivateUse);
2876 }
2877
contextMenuEvent(QContextMenuEvent * e)2878 void KateViewInternal::contextMenuEvent(QContextMenuEvent *e)
2879 {
2880 // calculate where to show the context menu
2881
2882 QPoint p = e->pos();
2883
2884 if (e->reason() == QContextMenuEvent::Keyboard) {
2885 makeVisible(m_displayCursor, 0);
2886 p = cursorCoordinates(false);
2887 p.rx() -= startX();
2888 } else if (!view()->selection() || view()->config()->persistentSelection()) {
2889 placeCursor(e->pos());
2890 }
2891
2892 // show it
2893 if (view()->contextMenu()) {
2894 view()->spellingMenu()->setUseMouseForMisspelledRange((e->reason() == QContextMenuEvent::Mouse));
2895 view()->contextMenu()->popup(mapToGlobal(p));
2896 e->accept();
2897 }
2898 }
2899
mousePressEvent(QMouseEvent * e)2900 void KateViewInternal::mousePressEvent(QMouseEvent *e)
2901 {
2902 // was an inline note clicked?
2903 const auto noteData = inlineNoteAt(e->globalPos());
2904 const KTextEditor::InlineNote note(noteData);
2905 if (note.position().isValid()) {
2906 note.provider()->inlineNoteActivated(noteData, e->button(), e->globalPos());
2907 return;
2908 }
2909
2910 // no -- continue with normal handling
2911 switch (e->button()) {
2912 case Qt::LeftButton:
2913
2914 m_selChangedByUser = false;
2915
2916 if (m_possibleTripleClick) {
2917 m_possibleTripleClick = false;
2918
2919 m_selectionMode = Line;
2920
2921 if (e->modifiers() & Qt::ShiftModifier) {
2922 updateSelection(m_cursor, true);
2923 } else {
2924 view()->selectLine(m_cursor);
2925 if (view()->selection()) {
2926 m_selectAnchor = view()->selectionRange().start();
2927 }
2928 }
2929
2930 if (view()->selection()) {
2931 QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection);
2932 }
2933
2934 // Keep the line at the select anchor selected during further
2935 // mouse selection
2936 if (m_selectAnchor.line() > view()->selectionRange().start().line()) {
2937 // Preserve the last selected line
2938 if (m_selectAnchor == view()->selectionRange().end() && m_selectAnchor.column() == 0) {
2939 m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line() - 1, 0));
2940 } else {
2941 m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line(), 0));
2942 }
2943 m_selectionCached.setEnd(view()->selectionRange().end());
2944 } else {
2945 // Preserve the first selected line
2946 m_selectionCached.setStart(view()->selectionRange().start());
2947 if (view()->selectionRange().end().line() > view()->selectionRange().start().line()) {
2948 m_selectionCached.setEnd(KTextEditor::Cursor(view()->selectionRange().start().line() + 1, 0));
2949 } else {
2950 m_selectionCached.setEnd(view()->selectionRange().end());
2951 }
2952 }
2953
2954 moveCursorToSelectionEdge();
2955
2956 m_scrollX = 0;
2957 m_scrollY = 0;
2958 m_scrollTimer.start(50);
2959
2960 e->accept();
2961 return;
2962 } else if (m_selectionMode == Default) {
2963 m_selectionMode = Mouse;
2964 }
2965
2966 // request the software keyboard, if any
2967 if (e->button() == Qt::LeftButton && qApp->autoSipEnabled()) {
2968 QStyle::RequestSoftwareInputPanel behavior = QStyle::RequestSoftwareInputPanel(style()->styleHint(QStyle::SH_RequestSoftwareInputPanel));
2969 if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) {
2970 QEvent event(QEvent::RequestSoftwareInputPanel);
2971 QApplication::sendEvent(this, &event);
2972 }
2973 }
2974
2975 if (e->modifiers() & Qt::ShiftModifier) {
2976 if (!m_selectAnchor.isValid()) {
2977 m_selectAnchor = m_cursor;
2978 }
2979 } else {
2980 m_selectionCached = KTextEditor::Range::invalid();
2981 }
2982
2983 if (view()->config()->textDragAndDrop() && !(e->modifiers() & Qt::ShiftModifier) && isTargetSelected(e->pos())) {
2984 m_dragInfo.state = diPending;
2985 m_dragInfo.start = e->pos();
2986 } else {
2987 m_dragInfo.state = diNone;
2988
2989 if (e->modifiers() & Qt::ShiftModifier) {
2990 placeCursor(e->pos(), true, false);
2991 if (m_selectionCached.start().isValid()) {
2992 if (m_cursor.toCursor() < m_selectionCached.start()) {
2993 m_selectAnchor = m_selectionCached.end();
2994 } else {
2995 m_selectAnchor = m_selectionCached.start();
2996 }
2997 }
2998 setSelection(KTextEditor::Range(m_selectAnchor, m_cursor));
2999 } else {
3000 placeCursor(e->pos());
3001 }
3002
3003 m_scrollX = 0;
3004 m_scrollY = 0;
3005
3006 m_scrollTimer.start(50);
3007 }
3008
3009 e->accept();
3010 break;
3011
3012 case Qt::RightButton:
3013 if (e->pos().x() == 0) {
3014 // Special handling for folding by right click
3015 placeCursor(e->pos());
3016 e->accept();
3017 }
3018 break;
3019
3020 default:
3021 e->ignore();
3022 break;
3023 }
3024 }
3025
mouseDoubleClickEvent(QMouseEvent * e)3026 void KateViewInternal::mouseDoubleClickEvent(QMouseEvent *e)
3027 {
3028 if (e->button() == Qt::LeftButton) {
3029 m_selectionMode = Word;
3030
3031 if (e->modifiers() & Qt::ShiftModifier) {
3032 // Now select the word under the select anchor
3033 int cs;
3034 int ce;
3035 Kate::TextLine l = doc()->kateTextLine(m_selectAnchor.line());
3036
3037 ce = m_selectAnchor.column();
3038 if (ce > 0 && doc()->highlight()->isInWord(l->at(ce))) {
3039 for (; ce < l->length(); ce++) {
3040 if (!doc()->highlight()->isInWord(l->at(ce))) {
3041 break;
3042 }
3043 }
3044 }
3045
3046 cs = m_selectAnchor.column() - 1;
3047 if (cs < doc()->lineLength(m_selectAnchor.line()) && doc()->highlight()->isInWord(l->at(cs))) {
3048 for (cs--; cs >= 0; cs--) {
3049 if (!doc()->highlight()->isInWord(l->at(cs))) {
3050 break;
3051 }
3052 }
3053 }
3054
3055 // ...and keep it selected
3056 if (cs + 1 < ce) {
3057 m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line(), cs + 1));
3058 m_selectionCached.setEnd(KTextEditor::Cursor(m_selectAnchor.line(), ce));
3059 } else {
3060 m_selectionCached.setStart(m_selectAnchor);
3061 m_selectionCached.setEnd(m_selectAnchor);
3062 }
3063 // Now word select to the mouse cursor
3064 placeCursor(e->pos(), true);
3065 } else {
3066 // first clear the selection, otherwise we run into bug #106402
3067 // ...and set the cursor position, for the same reason (otherwise there
3068 // are *other* idiosyncrasies we can't fix without reintroducing said
3069 // bug)
3070 // Parameters: don't redraw, and don't emit selectionChanged signal yet
3071 view()->clearSelection(false, false);
3072 placeCursor(e->pos());
3073 view()->selectWord(m_cursor);
3074 cursorToMatchingBracket(true);
3075
3076 if (view()->selection()) {
3077 m_selectAnchor = view()->selectionRange().start();
3078 m_selectionCached = view()->selectionRange();
3079 } else {
3080 m_selectAnchor = m_cursor;
3081 m_selectionCached = KTextEditor::Range(m_cursor, m_cursor);
3082 }
3083 }
3084
3085 // Move cursor to end (or beginning) of selected word
3086 #ifndef Q_OS_MACOS
3087 if (view()->selection()) {
3088 QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection);
3089 }
3090 #endif
3091
3092 moveCursorToSelectionEdge();
3093 m_possibleTripleClick = true;
3094 QTimer::singleShot(QApplication::doubleClickInterval(), this, SLOT(tripleClickTimeout()));
3095
3096 m_scrollX = 0;
3097 m_scrollY = 0;
3098
3099 m_scrollTimer.start(50);
3100
3101 e->accept();
3102 } else {
3103 e->ignore();
3104 }
3105 }
3106
tripleClickTimeout()3107 void KateViewInternal::tripleClickTimeout()
3108 {
3109 m_possibleTripleClick = false;
3110 }
3111
beginSelectLine(const QPoint & pos)3112 void KateViewInternal::beginSelectLine(const QPoint &pos)
3113 {
3114 placeCursor(pos);
3115 m_possibleTripleClick = true; // set so subsequent mousePressEvent will select line
3116 }
3117
mouseReleaseEvent(QMouseEvent * e)3118 void KateViewInternal::mouseReleaseEvent(QMouseEvent *e)
3119 {
3120 switch (e->button()) {
3121 case Qt::LeftButton:
3122 m_selectionMode = Default;
3123 // m_selectionCached.start().setLine( -1 );
3124
3125 if (m_selChangedByUser) {
3126 if (view()->selection()) {
3127 QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection);
3128 }
3129 moveCursorToSelectionEdge();
3130
3131 m_selChangedByUser = false;
3132 }
3133
3134 if (m_dragInfo.state == diPending) {
3135 placeCursor(e->pos(), e->modifiers() & Qt::ShiftModifier);
3136 } else if (m_dragInfo.state == diNone) {
3137 m_scrollTimer.stop();
3138 }
3139
3140 m_dragInfo.state = diNone;
3141
3142 e->accept();
3143 break;
3144
3145 case Qt::MiddleButton:
3146 if (!view()->config()->mousePasteAtCursorPosition()) {
3147 placeCursor(e->pos());
3148 }
3149
3150 if (doc()->isReadWrite()) {
3151 QString clipboard = QApplication::clipboard()->text(QClipboard::Selection);
3152 view()->paste(&clipboard);
3153 }
3154
3155 e->accept();
3156 break;
3157
3158 default:
3159 e->ignore();
3160 break;
3161 }
3162 }
3163
leaveEvent(QEvent *)3164 void KateViewInternal::leaveEvent(QEvent *)
3165 {
3166 m_textHintTimer.stop();
3167
3168 // fix bug 194452, scrolling keeps going if you scroll via mouse drag and press and other mouse
3169 // button outside the view area
3170 if (m_dragInfo.state == diNone) {
3171 m_scrollTimer.stop();
3172 }
3173
3174 hideBracketMatchPreview();
3175 }
3176
coordinatesToCursor(const QPoint & _coord,bool includeBorder) const3177 KTextEditor::Cursor KateViewInternal::coordinatesToCursor(const QPoint &_coord, bool includeBorder) const
3178 {
3179 QPoint coord(_coord);
3180
3181 KTextEditor::Cursor ret = KTextEditor::Cursor::invalid();
3182
3183 if (includeBorder) {
3184 coord.rx() -= m_leftBorder->width();
3185 }
3186 coord.rx() += startX();
3187
3188 const KateTextLayout &thisLine = yToKateTextLayout(coord.y());
3189 if (thisLine.isValid()) {
3190 ret = renderer()->xToCursor(thisLine, coord.x(), !view()->wrapCursor());
3191 }
3192
3193 if (ret.column() > view()->document()->lineLength(ret.line())) {
3194 // The cursor is beyond the end of the line; in that case the renderer
3195 // gives the index of the character behind the last one.
3196 return KTextEditor::Cursor::invalid();
3197 }
3198
3199 return ret;
3200 }
3201
mouseMoveEvent(QMouseEvent * e)3202 void KateViewInternal::mouseMoveEvent(QMouseEvent *e)
3203 {
3204 if (m_scroller->state() != QScroller::Inactive) {
3205 // Touchscreen is handled by scrollEvent()
3206 return;
3207 }
3208 KTextEditor::Cursor newPosition = coordinatesToCursor(e->pos(), false);
3209 if (newPosition != m_mouse) {
3210 m_mouse = newPosition;
3211 mouseMoved();
3212 }
3213
3214 if (e->buttons() == Qt::NoButton) {
3215 auto noteData = inlineNoteAt(e->globalPos());
3216 auto focusChanged = false;
3217 if (noteData.m_position.isValid()) {
3218 if (!m_activeInlineNote.m_position.isValid()) {
3219 // no active note -- focus in
3220 tagLine(noteData.m_position);
3221 focusChanged = true;
3222 noteData.m_underMouse = true;
3223 noteData.m_provider->inlineNoteFocusInEvent(KTextEditor::InlineNote(noteData), e->globalPos());
3224 m_activeInlineNote = noteData;
3225 } else {
3226 noteData.m_provider->inlineNoteMouseMoveEvent(KTextEditor::InlineNote(noteData), e->globalPos());
3227 }
3228 } else if (m_activeInlineNote.m_position.isValid()) {
3229 tagLine(m_activeInlineNote.m_position);
3230 focusChanged = true;
3231 m_activeInlineNote.m_underMouse = false;
3232 m_activeInlineNote.m_provider->inlineNoteFocusOutEvent(KTextEditor::InlineNote(m_activeInlineNote));
3233 m_activeInlineNote = {};
3234 }
3235 if (focusChanged) {
3236 // the note might change its appearance in reaction to the focus event
3237 updateDirty();
3238 }
3239 }
3240
3241 if (e->buttons() & Qt::LeftButton) {
3242 if (m_dragInfo.state == diPending) {
3243 // we had a mouse down, but haven't confirmed a drag yet
3244 // if the mouse has moved sufficiently, we will confirm
3245 QPoint p(e->pos() - m_dragInfo.start);
3246
3247 // we've left the drag square, we can start a real drag operation now
3248 if (p.manhattanLength() > QApplication::startDragDistance()) {
3249 doDrag();
3250 }
3251
3252 return;
3253 } else if (m_dragInfo.state == diDragging) {
3254 // Don't do anything after a canceled drag until the user lets go of
3255 // the mouse button!
3256 return;
3257 }
3258
3259 m_mouseX = e->x();
3260 m_mouseY = e->y();
3261
3262 m_scrollX = 0;
3263 m_scrollY = 0;
3264 int d = renderer()->lineHeight();
3265
3266 if (m_mouseX < 0) {
3267 m_scrollX = -d;
3268 }
3269
3270 if (m_mouseX > width()) {
3271 m_scrollX = d;
3272 }
3273
3274 if (m_mouseY < 0) {
3275 m_mouseY = 0;
3276 m_scrollY = -d;
3277 }
3278
3279 if (m_mouseY > height()) {
3280 m_mouseY = height();
3281 m_scrollY = d;
3282 }
3283
3284 if (!m_scrollY) {
3285 placeCursor(QPoint(m_mouseX, m_mouseY), true);
3286 }
3287
3288 } else {
3289 if (view()->config()->textDragAndDrop() && isTargetSelected(e->pos())) {
3290 // mouse is over selected text. indicate that the text is draggable by setting
3291 // the arrow cursor as other Qt text editing widgets do
3292 if (m_mouseCursor != Qt::ArrowCursor) {
3293 m_mouseCursor = Qt::ArrowCursor;
3294 setCursor(m_mouseCursor);
3295 }
3296 } else {
3297 // normal text cursor
3298 if (m_mouseCursor != Qt::IBeamCursor) {
3299 m_mouseCursor = Qt::IBeamCursor;
3300 setCursor(m_mouseCursor);
3301 }
3302 }
3303 // We need to check whether the mouse position is actually within the widget,
3304 // because other widgets like the icon border forward their events to this,
3305 // and we will create invalid text hint requests if we don't check
3306 if (textHintsEnabled() && geometry().contains(parentWidget()->mapFromGlobal(e->globalPos()))) {
3307 if (QToolTip::isVisible()) {
3308 QToolTip::hideText();
3309 }
3310 m_textHintTimer.start(m_textHintDelay);
3311 m_textHintPos = e->pos();
3312 }
3313 }
3314 }
3315
updateDirty()3316 void KateViewInternal::updateDirty()
3317 {
3318 const int h = renderer()->lineHeight();
3319
3320 int currentRectStart = -1;
3321 int currentRectEnd = -1;
3322
3323 QRegion updateRegion;
3324
3325 {
3326 for (int i = 0; i < cache()->viewCacheLineCount(); ++i) {
3327 if (cache()->viewLine(i).isDirty()) {
3328 if (currentRectStart == -1) {
3329 currentRectStart = h * i;
3330 currentRectEnd = h;
3331 } else {
3332 currentRectEnd += h;
3333 }
3334
3335 } else if (currentRectStart != -1) {
3336 updateRegion += QRect(0, currentRectStart, width(), currentRectEnd);
3337 currentRectStart = -1;
3338 currentRectEnd = -1;
3339 }
3340 }
3341 }
3342
3343 if (currentRectStart != -1) {
3344 updateRegion += QRect(0, currentRectStart, width(), currentRectEnd);
3345 }
3346
3347 if (!updateRegion.isEmpty()) {
3348 if (debugPainting) {
3349 qCDebug(LOG_KTE) << "Update dirty region " << updateRegion;
3350 }
3351 update(updateRegion);
3352 }
3353 }
3354
hideEvent(QHideEvent * e)3355 void KateViewInternal::hideEvent(QHideEvent *e)
3356 {
3357 Q_UNUSED(e);
3358 if (view()->isCompletionActive()) {
3359 view()->completionWidget()->abortCompletion();
3360 }
3361 }
3362
paintEvent(QPaintEvent * e)3363 void KateViewInternal::paintEvent(QPaintEvent *e)
3364 {
3365 if (debugPainting) {
3366 qCDebug(LOG_KTE) << "GOT PAINT EVENT: Region" << e->region();
3367 }
3368
3369 const QRect &unionRect = e->rect();
3370
3371 int xStart = startX() + unionRect.x();
3372 int xEnd = xStart + unionRect.width();
3373 uint h = renderer()->lineHeight();
3374 uint startz = (unionRect.y() / h);
3375 uint endz = startz + 1 + (unionRect.height() / h);
3376 uint lineRangesSize = cache()->viewCacheLineCount();
3377 const KTextEditor::Cursor pos = m_cursor;
3378
3379 QPainter paint(this);
3380
3381 // THIS IS ULTRA EVIL AND ADDS STRANGE RENDERING ARTIFACTS WITH SCALING!!!!
3382 // SEE BUG https://bugreports.qt.io/browse/QTBUG-66036
3383 // paint.setRenderHints(QPainter::TextAntialiasing);
3384
3385 paint.save();
3386
3387 renderer()->setCaretStyle(m_currentInputMode->caretStyle());
3388 renderer()->setShowTabs(doc()->config()->showTabs());
3389 renderer()->setShowSpaces(doc()->config()->showSpaces());
3390 renderer()->updateMarkerSize();
3391
3392 // paint line by line
3393 // this includes parts that span areas without real lines
3394 // translate to first line to paint
3395 paint.translate(unionRect.x(), startz * h);
3396 for (uint z = startz; z <= endz; z++) {
3397 // paint regions without lines mapped to
3398 if ((z >= lineRangesSize) || (cache()->viewLine(z).line() == -1)) {
3399 if (!(z >= lineRangesSize)) {
3400 cache()->viewLine(z).setDirty(false);
3401 }
3402 paint.fillRect(0, 0, unionRect.width(), h, renderer()->config()->backgroundColor());
3403 }
3404
3405 // paint text lines
3406 else {
3407 // If viewLine() returns non-zero, then a document line was split
3408 // in several visual lines, and we're trying to paint visual line
3409 // that is not the first. In that case, this line was already
3410 // painted previously, since KateRenderer::paintTextLine paints
3411 // all visual lines.
3412 //
3413 // Except if we're at the start of the region that needs to
3414 // be painted -- when no previous calls to paintTextLine were made.
3415 KateTextLayout &thisLine = cache()->viewLine(z);
3416 if (!thisLine.viewLine() || z == startz) {
3417 // paint our line
3418 // set clipping region to only paint the relevant parts
3419 paint.save();
3420 paint.translate(QPoint(0, h * -thisLine.viewLine()));
3421
3422 // compute rect for line, fill the stuff
3423 // important: as we allow some ARGB colors for other stuff, it is REALLY important to fill the full range once!
3424 const QRectF lineRect(0, 0, unionRect.width(), h * thisLine.kateLineLayout()->viewLineCount());
3425 paint.fillRect(lineRect, renderer()->config()->backgroundColor());
3426
3427 // THIS IS ULTRA EVIL AND ADDS STRANGE RENDERING ARTIFACTS WITH SCALING!!!!
3428 // SEE BUG https://bugreports.qt.io/browse/QTBUG-66036
3429 // => using a QRectF solves the cut of 1 pixel, the same call with QRect does create artifacts!
3430 paint.setClipRect(lineRect);
3431 renderer()->paintTextLine(paint, thisLine.kateLineLayout(), xStart, xEnd, &pos);
3432 paint.restore();
3433
3434 // line painted, reset and state + mark line as non-dirty
3435 thisLine.setDirty(false);
3436 }
3437 }
3438
3439 // translate to next line
3440 paint.translate(0, h);
3441 }
3442
3443 paint.restore();
3444
3445 if (m_textAnimation) {
3446 m_textAnimation->draw(paint);
3447 }
3448 }
3449
resizeEvent(QResizeEvent * e)3450 void KateViewInternal::resizeEvent(QResizeEvent *e)
3451 {
3452 bool expandedHorizontally = width() > e->oldSize().width();
3453 bool expandedVertically = height() > e->oldSize().height();
3454 bool heightChanged = height() != e->oldSize().height();
3455
3456 m_dummy->setFixedSize(m_lineScroll->width(), m_columnScroll->sizeHint().height());
3457 m_madeVisible = false;
3458
3459 // resize the bracket match preview
3460 if (m_bmPreview) {
3461 showBracketMatchPreview();
3462 }
3463
3464 if (heightChanged) {
3465 setAutoCenterLines(m_autoCenterLines, false);
3466 m_cachedMaxStartPos.setPosition(-1, -1);
3467 }
3468
3469 if (view()->dynWordWrap()) {
3470 bool dirtied = false;
3471
3472 for (int i = 0; i < cache()->viewCacheLineCount(); i++) {
3473 // find the first dirty line
3474 // the word wrap updateView algorithm is forced to check all lines after a dirty one
3475 KateTextLayout viewLine = cache()->viewLine(i);
3476
3477 if (viewLine.wrap() || viewLine.isRightToLeft() || viewLine.width() > width()) {
3478 dirtied = true;
3479 viewLine.setDirty();
3480 break;
3481 }
3482 }
3483
3484 if (dirtied || heightChanged) {
3485 updateView(true);
3486 m_leftBorder->update();
3487 }
3488 } else {
3489 updateView();
3490
3491 if (expandedHorizontally && startX() > 0) {
3492 scrollColumns(startX() - (width() - e->oldSize().width()));
3493 }
3494 }
3495
3496 if (width() < e->oldSize().width() && !view()->wrapCursor()) {
3497 // May have to restrain cursor to new smaller width...
3498 if (m_cursor.column() > doc()->lineLength(m_cursor.line())) {
3499 KateTextLayout thisLine = currentLayout();
3500
3501 KTextEditor::Cursor newCursor(m_cursor.line(),
3502 thisLine.endCol() + ((width() - thisLine.xOffset() - (thisLine.width() - startX())) / renderer()->spaceWidth()) - 1);
3503 if (newCursor.column() < m_cursor.column()) {
3504 updateCursor(newCursor);
3505 }
3506 }
3507 }
3508
3509 if (expandedVertically) {
3510 KTextEditor::Cursor max = maxStartPos();
3511 if (startPos() > max) {
3512 scrollPos(max);
3513 return; // already fired displayRangeChanged
3514 }
3515 }
3516 Q_EMIT view()->displayRangeChanged(m_view);
3517 }
3518
moveEvent(QMoveEvent * e)3519 void KateViewInternal::moveEvent(QMoveEvent *e)
3520 {
3521 // move the bracket match preview to the new location
3522 if (e->pos() != e->oldPos() && m_bmPreview) {
3523 showBracketMatchPreview();
3524 }
3525
3526 QWidget::moveEvent(e);
3527 }
3528
scrollTimeout()3529 void KateViewInternal::scrollTimeout()
3530 {
3531 if (m_scrollX || m_scrollY) {
3532 const int scrollTo = startPos().line() + (m_scrollY / (int)renderer()->lineHeight());
3533 placeCursor(QPoint(m_mouseX, m_mouseY), true);
3534 scrollLines(scrollTo);
3535 }
3536 }
3537
cursorTimeout()3538 void KateViewInternal::cursorTimeout()
3539 {
3540 if (!debugPainting && m_currentInputMode->blinkCaret()) {
3541 renderer()->setDrawCaret(!renderer()->drawCaret());
3542 paintCursor();
3543 }
3544 }
3545
textHintTimeout()3546 void KateViewInternal::textHintTimeout()
3547 {
3548 m_textHintTimer.stop();
3549
3550 KTextEditor::Cursor c = coordinatesToCursor(m_textHintPos, false);
3551 if (!c.isValid()) {
3552 return;
3553 }
3554
3555 QStringList textHints;
3556 for (KTextEditor::TextHintProvider *const p : m_textHintProviders) {
3557 if (!p) {
3558 continue;
3559 }
3560
3561 const QString hint = p->textHint(m_view, c);
3562 if (!hint.isEmpty()) {
3563 textHints.append(hint);
3564 }
3565 }
3566
3567 if (!textHints.isEmpty()) {
3568 qCDebug(LOG_KTE) << "Hint text: " << textHints;
3569 QString hint;
3570 for (const QString &str : std::as_const(textHints)) {
3571 hint += QStringLiteral("<p>%1</p>").arg(str);
3572 }
3573 QPoint pos(startX() + m_textHintPos.x(), m_textHintPos.y());
3574 QToolTip::showText(mapToGlobal(pos), hint);
3575 }
3576 }
3577
focusInEvent(QFocusEvent *)3578 void KateViewInternal::focusInEvent(QFocusEvent *)
3579 {
3580 if (QApplication::cursorFlashTime() > 0) {
3581 m_cursorTimer.start(QApplication::cursorFlashTime() / 2);
3582 }
3583
3584 paintCursor();
3585
3586 doc()->setActiveView(m_view);
3587
3588 // this will handle focus stuff in kateview
3589 view()->slotGotFocus();
3590 }
3591
focusOutEvent(QFocusEvent *)3592 void KateViewInternal::focusOutEvent(QFocusEvent *)
3593 {
3594 // if (view()->isCompletionActive())
3595 // view()->abortCompletion();
3596
3597 m_cursorTimer.stop();
3598 view()->renderer()->setDrawCaret(true);
3599 paintCursor();
3600
3601 m_textHintTimer.stop();
3602
3603 view()->slotLostFocus();
3604
3605 hideBracketMatchPreview();
3606 }
3607
doDrag()3608 void KateViewInternal::doDrag()
3609 {
3610 m_dragInfo.state = diDragging;
3611 m_dragInfo.dragObject = new QDrag(this);
3612 std::unique_ptr<QMimeData> mimeData(new QMimeData());
3613 mimeData->setText(view()->selectionText());
3614
3615 const auto startCur = view()->selectionRange().start();
3616 const auto endCur = view()->selectionRange().end();
3617 if (!startCur.isValid() || !endCur.isValid()) {
3618 return;
3619 }
3620
3621 int startLine = startCur.line();
3622 int endLine = endCur.line();
3623
3624 /**
3625 * Get real first and last visible line nos.
3626 * This is important as startLine() / endLine() are virtual and we can't use
3627 * them here
3628 */
3629 const int firstVisibleLine = view()->firstDisplayedLineInternal(KTextEditor::View::RealLine);
3630 const int lastVisibleLine = view()->lastDisplayedLineInternal(KTextEditor::View::RealLine);
3631
3632 // get visible selected lines
3633 for (int l = startLine; l <= endLine; ++l) {
3634 if (l >= firstVisibleLine) {
3635 break;
3636 }
3637 ++startLine;
3638 }
3639 for (int l = endLine; l >= startLine; --l) {
3640 if (l <= lastVisibleLine) {
3641 break;
3642 }
3643 --endLine;
3644 }
3645
3646 // calculate the height / width / scale
3647 int w = 0;
3648 int h = 0;
3649 const QFontMetricsF &fm = renderer()->currentFontMetrics();
3650 for (int l = startLine; l <= endLine; ++l) {
3651 w = std::max((int)fm.horizontalAdvance(doc()->line(l)), w);
3652 h += fm.height();
3653 }
3654 qreal scale = h > m_view->height() / 2 ? 0.75 : 1.0;
3655
3656 // Calculate start x pos on start line
3657 int sX = 0;
3658 if (startLine == startCur.line()) {
3659 sX = renderer()->cursorToX(cache()->textLayout(startCur), startCur, !view()->wrapCursor());
3660 }
3661
3662 // Calculate end x pos on end line
3663 int eX = 0;
3664 if (endLine == endCur.line()) {
3665 eX = renderer()->cursorToX(cache()->textLayout(endCur), endCur, !view()->wrapCursor());
3666 }
3667
3668 // Create a pixmap this selection
3669 const qreal dpr = devicePixelRatioF();
3670 QPixmap pixmap(w * dpr, h * dpr);
3671 if (!pixmap.isNull()) {
3672 pixmap.setDevicePixelRatio(dpr);
3673 pixmap.fill(Qt::transparent);
3674 renderer()->paintSelection(&pixmap, startLine, sX, endLine, eX, scale);
3675 }
3676
3677 // Calculate position where pixmap will appear when user
3678 // starts dragging
3679 const int x = 0;
3680 /**
3681 * lineToVisibleLine() = real line => virtual line
3682 * This is necessary here because if there is a folding in the current
3683 * view lines, the y pos can be incorrect. So, we make sure to convert
3684 * it to virtual line before calculating y
3685 */
3686 const int y = lineToY(view()->m_textFolding.lineToVisibleLine(startLine));
3687 const QPoint pos = mapFromGlobal(QCursor::pos()) - QPoint(x, y);
3688
3689 m_dragInfo.dragObject->setPixmap(pixmap);
3690 m_dragInfo.dragObject->setHotSpot(pos);
3691 m_dragInfo.dragObject->setMimeData(mimeData.release());
3692 m_dragInfo.dragObject->exec(Qt::MoveAction | Qt::CopyAction);
3693 }
3694
dragEnterEvent(QDragEnterEvent * event)3695 void KateViewInternal::dragEnterEvent(QDragEnterEvent *event)
3696 {
3697 if (event->source() == this) {
3698 event->setDropAction(Qt::MoveAction);
3699 }
3700 event->setAccepted((event->mimeData()->hasText() && doc()->isReadWrite()) || event->mimeData()->hasUrls());
3701 }
3702
fixDropEvent(QDropEvent * event)3703 void KateViewInternal::fixDropEvent(QDropEvent *event)
3704 {
3705 if (event->source() != this) {
3706 event->setDropAction(Qt::CopyAction);
3707 } else {
3708 Qt::DropAction action = Qt::MoveAction;
3709 #ifdef Q_WS_MAC
3710 if (event->keyboardModifiers() & Qt::AltModifier) {
3711 action = Qt::CopyAction;
3712 }
3713 #else
3714 if (event->keyboardModifiers() & Qt::ControlModifier) {
3715 action = Qt::CopyAction;
3716 }
3717 #endif
3718 event->setDropAction(action);
3719 }
3720 }
3721
dragMoveEvent(QDragMoveEvent * event)3722 void KateViewInternal::dragMoveEvent(QDragMoveEvent *event)
3723 {
3724 // track the cursor to the current drop location
3725 placeCursor(event->pos(), true, false);
3726
3727 // important: accept action to switch between copy and move mode
3728 // without this, the text will always be copied.
3729 fixDropEvent(event);
3730 }
3731
dropEvent(QDropEvent * event)3732 void KateViewInternal::dropEvent(QDropEvent *event)
3733 {
3734 // if we have urls, pass this event off to the hosting application
3735 if (event->mimeData()->hasUrls()) {
3736 Q_EMIT dropEventPass(event);
3737 return;
3738 }
3739
3740 if (event->mimeData()->hasText() && doc()->isReadWrite()) {
3741 const QString text = event->mimeData()->text();
3742 const bool blockMode = view()->blockSelection();
3743
3744 fixDropEvent(event);
3745
3746 // Remember where to paste/move...
3747 KTextEditor::Cursor targetCursor(m_cursor);
3748 // Use powerful MovingCursor to track our changes we may do
3749 std::unique_ptr<KTextEditor::MovingCursor> targetCursor2(doc()->newMovingCursor(m_cursor));
3750
3751 // As always need the BlockMode some special treatment
3752 const KTextEditor::Range selRange(view()->selectionRange());
3753 const KTextEditor::Cursor blockAdjust(selRange.numberOfLines(), selRange.columnWidth());
3754
3755 // Restore the cursor position before editStart(), so that it is correctly stored for the undo action
3756 if (event->dropAction() != Qt::CopyAction) {
3757 editSetCursor(selRange.end());
3758 } else {
3759 view()->clearSelection();
3760 }
3761
3762 // use one transaction
3763 doc()->editStart();
3764
3765 if (event->dropAction() != Qt::CopyAction) {
3766 view()->removeSelectedText();
3767 if (targetCursor2->toCursor() != targetCursor) {
3768 // Hm, multi line selection moved down, we need to adjust our dumb cursor
3769 targetCursor = targetCursor2->toCursor();
3770 }
3771 doc()->insertText(targetCursor2->toCursor(), text, blockMode);
3772
3773 } else {
3774 doc()->insertText(targetCursor, text, blockMode);
3775 }
3776
3777 if (blockMode) {
3778 setSelection(KTextEditor::Range(targetCursor, targetCursor + blockAdjust));
3779 editSetCursor(targetCursor + blockAdjust);
3780 } else {
3781 setSelection(KTextEditor::Range(targetCursor, targetCursor2->toCursor()));
3782 editSetCursor(targetCursor2->toCursor()); // Just to satisfy autotest
3783 }
3784
3785 doc()->editEnd();
3786
3787 event->acceptProposedAction();
3788 updateView();
3789 }
3790
3791 // finally finish drag and drop mode
3792 m_dragInfo.state = diNone;
3793 // important, because the eventFilter`s DragLeave does not occur
3794 stopDragScroll();
3795 }
3796 // END EVENT HANDLING STUFF
3797
clear()3798 void KateViewInternal::clear()
3799 {
3800 m_startPos.setPosition(0, 0);
3801 m_displayCursor = KTextEditor::Cursor(0, 0);
3802 m_cursor.setPosition(0, 0);
3803 cache()->clear();
3804 updateView(true);
3805 m_lineScroll->updatePixmap();
3806 }
3807
wheelEvent(QWheelEvent * e)3808 void KateViewInternal::wheelEvent(QWheelEvent *e)
3809 {
3810 // check if this event should change the font size (Ctrl pressed, angle reported and not accidentally so)
3811 // Note: if detectZoomingEvent() doesn't unset the ControlModifier we'll get accelerated scrolling.
3812 if (m_zoomEventFilter->detectZoomingEvent(e)) {
3813 if (e->angleDelta().y() > 0) {
3814 slotIncFontSizes(qreal(e->angleDelta().y()) / QWheelEvent::DefaultDeltasPerStep);
3815 } else if (e->angleDelta().y() < 0) {
3816 slotDecFontSizes(qreal(-e->angleDelta().y()) / QWheelEvent::DefaultDeltasPerStep);
3817 }
3818
3819 // accept always and be done for zooming
3820 e->accept();
3821 return;
3822 }
3823
3824 // handle vertical scrolling via the scrollbar
3825 if (e->angleDelta().y() != 0) {
3826 // compute distance
3827 auto sign = m_lineScroll->invertedControls() ? -1 : 1;
3828 auto offset = sign * qreal(e->angleDelta().y()) / 120.0;
3829 if (e->modifiers() & Qt::ShiftModifier) {
3830 const auto pageStep = m_lineScroll->pageStep();
3831 offset = qBound(-pageStep, int(offset * pageStep), pageStep);
3832 } else {
3833 offset *= QApplication::wheelScrollLines();
3834 }
3835
3836 // handle accumulation
3837 m_accumulatedScroll += offset - int(offset);
3838 const auto extraAccumulated = int(m_accumulatedScroll);
3839 m_accumulatedScroll -= extraAccumulated;
3840
3841 // do scroll
3842 scrollViewLines(int(offset) + extraAccumulated);
3843 e->accept();
3844 }
3845
3846 // handle horizontal scrolling via the scrollbar
3847 if (e->angleDelta().x() != 0) {
3848 // if we have dyn word wrap, we should ignore the scroll events
3849 if (view()->dynWordWrap()) {
3850 e->accept();
3851 return;
3852 }
3853
3854 // if we scroll up/down we do not want to trigger unintended sideways scrolls
3855 if (qAbs(e->angleDelta().y()) > qAbs(e->angleDelta().x())) {
3856 e->accept();
3857 return;
3858 }
3859
3860 QWheelEvent copy = *e;
3861 QApplication::sendEvent(m_columnScroll, ©);
3862 if (copy.isAccepted()) {
3863 e->accept();
3864 }
3865 }
3866
3867 // hide bracket match preview so that it won't linger while scrolling'
3868 hideBracketMatchPreview();
3869 }
3870
scrollPrepareEvent(QScrollPrepareEvent * event)3871 void KateViewInternal::scrollPrepareEvent(QScrollPrepareEvent *event)
3872 {
3873 int lineHeight = renderer()->lineHeight();
3874 event->setViewportSize(QSizeF(0.0, 0.0));
3875 event->setContentPosRange(QRectF(0.0, 0.0, 0.0, m_lineScroll->maximum() * lineHeight));
3876 event->setContentPos(QPointF(0.0, m_lineScroll->value() * lineHeight));
3877 event->accept();
3878 }
3879
scrollEvent(QScrollEvent * event)3880 void KateViewInternal::scrollEvent(QScrollEvent *event)
3881 {
3882 // FIXME Add horizontal scrolling, overscroll, scroll between lines, and word wrap awareness
3883 KTextEditor::Cursor newPos((int) event->contentPos().y() / renderer()->lineHeight(), 0);
3884 scrollPos(newPos);
3885 event->accept();
3886 }
3887
startDragScroll()3888 void KateViewInternal::startDragScroll()
3889 {
3890 if (!m_dragScrollTimer.isActive()) {
3891 m_dragScrollTimer.start(s_scrollTime);
3892 }
3893 }
3894
stopDragScroll()3895 void KateViewInternal::stopDragScroll()
3896 {
3897 m_dragScrollTimer.stop();
3898 updateView();
3899 }
3900
doDragScroll()3901 void KateViewInternal::doDragScroll()
3902 {
3903 QPoint p = this->mapFromGlobal(QCursor::pos());
3904
3905 int dx = 0;
3906 int dy = 0;
3907 if (p.y() < s_scrollMargin) {
3908 dy = p.y() - s_scrollMargin;
3909 } else if (p.y() > height() - s_scrollMargin) {
3910 dy = s_scrollMargin - (height() - p.y());
3911 }
3912
3913 if (p.x() < s_scrollMargin) {
3914 dx = p.x() - s_scrollMargin;
3915 } else if (p.x() > width() - s_scrollMargin) {
3916 dx = s_scrollMargin - (width() - p.x());
3917 }
3918
3919 dy /= 4;
3920
3921 if (dy) {
3922 scrollLines(startLine() + dy);
3923 }
3924
3925 if (columnScrollingPossible() && dx) {
3926 scrollColumns(qMin(startX() + dx, m_columnScroll->maximum()));
3927 }
3928
3929 if (!dy && !dx) {
3930 stopDragScroll();
3931 }
3932 }
3933
registerTextHintProvider(KTextEditor::TextHintProvider * provider)3934 void KateViewInternal::registerTextHintProvider(KTextEditor::TextHintProvider *provider)
3935 {
3936 if (std::find(m_textHintProviders.cbegin(), m_textHintProviders.cend(), provider) == m_textHintProviders.cend()) {
3937 m_textHintProviders.push_back(provider);
3938 }
3939
3940 // we have a client, so start timeout
3941 m_textHintTimer.start(m_textHintDelay);
3942 }
3943
unregisterTextHintProvider(KTextEditor::TextHintProvider * provider)3944 void KateViewInternal::unregisterTextHintProvider(KTextEditor::TextHintProvider *provider)
3945 {
3946 const auto it = std::find(m_textHintProviders.cbegin(), m_textHintProviders.cend(), provider);
3947 if (it != m_textHintProviders.cend()) {
3948 m_textHintProviders.erase(it);
3949 }
3950
3951 if (m_textHintProviders.empty()) {
3952 m_textHintTimer.stop();
3953 }
3954 }
3955
setTextHintDelay(int delay)3956 void KateViewInternal::setTextHintDelay(int delay)
3957 {
3958 if (delay <= 0) {
3959 m_textHintDelay = 200; // ms
3960 } else {
3961 m_textHintDelay = delay; // ms
3962 }
3963 }
3964
textHintDelay() const3965 int KateViewInternal::textHintDelay() const
3966 {
3967 return m_textHintDelay;
3968 }
3969
textHintsEnabled()3970 bool KateViewInternal::textHintsEnabled()
3971 {
3972 return !m_textHintProviders.empty();
3973 }
3974
3975 // BEGIN EDIT STUFF
editStart()3976 void KateViewInternal::editStart()
3977 {
3978 editSessionNumber++;
3979
3980 if (editSessionNumber > 1) {
3981 return;
3982 }
3983
3984 editIsRunning = true;
3985 editOldCursor = m_cursor;
3986 editOldSelection = view()->selectionRange();
3987 }
3988
editEnd(int editTagLineStart,int editTagLineEnd,bool tagFrom)3989 void KateViewInternal::editEnd(int editTagLineStart, int editTagLineEnd, bool tagFrom)
3990 {
3991 if (editSessionNumber == 0) {
3992 return;
3993 }
3994
3995 editSessionNumber--;
3996
3997 if (editSessionNumber > 0) {
3998 return;
3999 }
4000
4001 // fix start position, might have moved from column 0
4002 // try to clever calculate the right start column for the tricky dyn word wrap case
4003 int col = 0;
4004 if (view()->dynWordWrap()) {
4005 if (KateLineLayoutPtr layout = cache()->line(startLine())) {
4006 int index = layout->viewLineForColumn(startPos().column());
4007 if (index >= 0 && index < layout->viewLineCount()) {
4008 col = layout->viewLine(index).startCol();
4009 }
4010 }
4011 }
4012 m_startPos.setPosition(startLine(), col);
4013
4014 if (tagFrom && (editTagLineStart <= int(view()->textFolding().visibleLineToLine(startLine())))) {
4015 tagAll();
4016 } else {
4017 tagLines(editTagLineStart, tagFrom ? qMax(doc()->lastLine() + 1, editTagLineEnd) : editTagLineEnd, true);
4018 }
4019
4020 if (editOldCursor == m_cursor.toCursor()) {
4021 updateBracketMarks();
4022 }
4023
4024 updateView(true);
4025
4026 if (editOldCursor != m_cursor.toCursor() || m_view == doc()->activeView()) {
4027 // Only scroll the view to the cursor if the insertion happens at the cursor.
4028 // This might not be the case for e.g. collaborative editing, when a remote user
4029 // inserts text at a position not at the caret.
4030 if (m_cursor.line() >= editTagLineStart && m_cursor.line() <= editTagLineEnd) {
4031 m_madeVisible = false;
4032 updateCursor(m_cursor, true);
4033 }
4034 }
4035
4036 // selection changed?
4037 // fixes bug 316226
4038 if (editOldSelection != view()->selectionRange()
4039 || (editOldSelection.isValid() && !editOldSelection.isEmpty()
4040 && !(editTagLineStart > editOldSelection.end().line() && editTagLineEnd < editOldSelection.start().line()))) {
4041 Q_EMIT view()->selectionChanged(m_view);
4042 }
4043
4044 editIsRunning = false;
4045 }
4046
editSetCursor(const KTextEditor::Cursor _cursor)4047 void KateViewInternal::editSetCursor(const KTextEditor::Cursor _cursor)
4048 {
4049 if (m_cursor.toCursor() != _cursor) {
4050 m_cursor.setPosition(_cursor);
4051 }
4052 }
4053 // END
4054
viewSelectionChanged()4055 void KateViewInternal::viewSelectionChanged()
4056 {
4057 if (!view()->selection()) {
4058 m_selectAnchor = KTextEditor::Cursor::invalid();
4059 } else {
4060 m_selectAnchor = view()->selectionRange().start();
4061 }
4062 // Do NOT nuke the entire range! The reason is that a shift+DC selection
4063 // might (correctly) set the range to be empty (i.e. start() == end()), and
4064 // subsequent dragging might shrink the selection into non-existence. When
4065 // this happens, we use the cached end to restore the cached start so that
4066 // updateSelection is not confused. See also comments in updateSelection.
4067 m_selectionCached.setStart(KTextEditor::Cursor::invalid());
4068 }
4069
isUserSelecting() const4070 bool KateViewInternal::isUserSelecting() const
4071 {
4072 return m_selChangedByUser;
4073 }
4074
cache() const4075 KateLayoutCache *KateViewInternal::cache() const
4076 {
4077 return m_layoutCache;
4078 }
4079
toRealCursor(const KTextEditor::Cursor virtualCursor) const4080 KTextEditor::Cursor KateViewInternal::toRealCursor(const KTextEditor::Cursor virtualCursor) const
4081 {
4082 return KTextEditor::Cursor(view()->textFolding().visibleLineToLine(virtualCursor.line()), virtualCursor.column());
4083 }
4084
toVirtualCursor(const KTextEditor::Cursor realCursor) const4085 KTextEditor::Cursor KateViewInternal::toVirtualCursor(const KTextEditor::Cursor realCursor) const
4086 {
4087 // only convert valid lines, folding doesn't like invalid input!
4088 // don't validate whole cursor, column might be -1
4089 if (realCursor.line() < 0) {
4090 return KTextEditor::Cursor::invalid();
4091 }
4092
4093 return KTextEditor::Cursor(view()->textFolding().lineToVisibleLine(realCursor.line()), realCursor.column());
4094 }
4095
renderer() const4096 KateRenderer *KateViewInternal::renderer() const
4097 {
4098 return view()->renderer();
4099 }
4100
mouseMoved()4101 void KateViewInternal::mouseMoved()
4102 {
4103 view()->notifyMousePositionChanged(m_mouse);
4104 view()->updateRangesIn(KTextEditor::Attribute::ActivateMouseIn);
4105 }
4106
cursorMoved()4107 void KateViewInternal::cursorMoved()
4108 {
4109 view()->updateRangesIn(KTextEditor::Attribute::ActivateCaretIn);
4110
4111 #ifndef QT_NO_ACCESSIBILITY
4112 if (QAccessible::isActive()) {
4113 QAccessibleTextCursorEvent ev(this, static_cast<KateViewAccessible *>(QAccessible::queryAccessibleInterface(this))->positionFromCursor(this, m_cursor));
4114 QAccessible::updateAccessibility(&ev);
4115 }
4116 #endif
4117 }
4118
doc()4119 KTextEditor::DocumentPrivate *KateViewInternal::doc()
4120 {
4121 return m_view->doc();
4122 }
4123
doc() const4124 KTextEditor::DocumentPrivate *KateViewInternal::doc() const
4125 {
4126 return m_view->doc();
4127 }
4128
rangeAffectsView(const KTextEditor::Range & range,bool realCursors) const4129 bool KateViewInternal::rangeAffectsView(const KTextEditor::Range &range, bool realCursors) const
4130 {
4131 int startLine = KateViewInternal::startLine();
4132 int endLine = startLine + (int)m_visibleLineCount;
4133
4134 if (realCursors) {
4135 startLine = (int)view()->textFolding().visibleLineToLine(startLine);
4136 endLine = (int)view()->textFolding().visibleLineToLine(endLine);
4137 }
4138
4139 return (range.end().line() >= startLine) || (range.start().line() <= endLine);
4140 }
4141
4142 // BEGIN IM INPUT STUFF
inputMethodQuery(Qt::InputMethodQuery query) const4143 QVariant KateViewInternal::inputMethodQuery(Qt::InputMethodQuery query) const
4144 {
4145 switch (query) {
4146 case Qt::ImCursorRectangle: {
4147 // Cursor placement code is changed for Asian input method that
4148 // shows candidate window. This behavior is same as Qt/E 2.3.7
4149 // which supports Asian input methods. Asian input methods need
4150 // start point of IM selection text to place candidate window as
4151 // adjacent to the selection text.
4152 //
4153 // in Qt5, cursor rectangle is used as QRectF internally, and it
4154 // will be checked by QRectF::isValid(), which will mark rectangle
4155 // with width == 0 or height == 0 as invalid.
4156 auto lineHeight = renderer()->lineHeight();
4157 return QRect(cursorToCoordinate(m_cursor, true, false), QSize(1, lineHeight ? lineHeight : 1));
4158 }
4159
4160 case Qt::ImFont:
4161 return renderer()->currentFont();
4162
4163 case Qt::ImCursorPosition:
4164 return m_cursor.column();
4165
4166 case Qt::ImAnchorPosition:
4167 // If selectAnchor is at the same line, return the real anchor position
4168 // Otherwise return the same position of cursor
4169 if (view()->selection() && m_selectAnchor.line() == m_cursor.line()) {
4170 return m_selectAnchor.column();
4171 } else {
4172 return m_cursor.column();
4173 }
4174
4175 case Qt::ImSurroundingText:
4176 if (Kate::TextLine l = doc()->kateTextLine(m_cursor.line())) {
4177 return l->string();
4178 } else {
4179 return QString();
4180 }
4181
4182 case Qt::ImCurrentSelection:
4183 if (view()->selection()) {
4184 return view()->selectionText();
4185 } else {
4186 return QString();
4187 }
4188 default:
4189 /* values: ImMaximumTextLength */
4190 break;
4191 }
4192
4193 return QWidget::inputMethodQuery(query);
4194 }
4195
inputMethodEvent(QInputMethodEvent * e)4196 void KateViewInternal::inputMethodEvent(QInputMethodEvent *e)
4197 {
4198 if (doc()->readOnly()) {
4199 e->ignore();
4200 return;
4201 }
4202
4203 // qCDebug(LOG_KTE) << "Event: cursor" << m_cursor << "commit" << e->commitString() << "preedit" << e->preeditString() << "replacement start" <<
4204 // e->replacementStart() << "length" << e->replacementLength();
4205
4206 if (!m_imPreeditRange) {
4207 m_imPreeditRange.reset(
4208 doc()->newMovingRange(KTextEditor::Range(m_cursor, m_cursor), KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight));
4209 }
4210
4211 if (!m_imPreeditRange->toRange().isEmpty()) {
4212 doc()->inputMethodStart();
4213 doc()->removeText(*m_imPreeditRange);
4214 doc()->inputMethodEnd();
4215 }
4216
4217 if (!e->commitString().isEmpty() || e->replacementLength() || !e->preeditString().isEmpty()) {
4218 view()->removeSelectedText();
4219 }
4220
4221 if (!e->commitString().isEmpty() || e->replacementLength()) {
4222 KTextEditor::Range preeditRange = *m_imPreeditRange;
4223
4224 KTextEditor::Cursor start(m_imPreeditRange->start().line(), m_imPreeditRange->start().column() + e->replacementStart());
4225 KTextEditor::Cursor removeEnd = start + KTextEditor::Cursor(0, e->replacementLength());
4226
4227 doc()->editStart();
4228 if (start != removeEnd) {
4229 doc()->removeText(KTextEditor::Range(start, removeEnd));
4230 }
4231
4232 // if the input method event is text that should be inserted, call KTextEditor::DocumentPrivate::typeChars()
4233 // with the text. that method will handle the input and take care of overwrite mode, etc.
4234 doc()->typeChars(m_view, e->commitString());
4235
4236 doc()->editEnd();
4237
4238 // Revert to the same range as above
4239 m_imPreeditRange->setRange(preeditRange);
4240 }
4241
4242 if (!e->preeditString().isEmpty()) {
4243 doc()->inputMethodStart();
4244 doc()->insertText(m_imPreeditRange->start(), e->preeditString());
4245 doc()->inputMethodEnd();
4246 // The preedit range gets automatically repositioned
4247 }
4248
4249 // Finished this input method context?
4250 if (m_imPreeditRange && e->preeditString().isEmpty()) {
4251 // delete the range and reset the pointer
4252 m_imPreeditRange.reset();
4253 m_imPreeditRangeChildren.clear();
4254
4255 if (QApplication::cursorFlashTime() > 0) {
4256 renderer()->setDrawCaret(false);
4257 }
4258 renderer()->setCaretOverrideColor(QColor());
4259
4260 e->accept();
4261 return;
4262 }
4263
4264 KTextEditor::Cursor newCursor = m_cursor;
4265 bool hideCursor = false;
4266 QColor caretColor;
4267
4268 if (m_imPreeditRange) {
4269 m_imPreeditRangeChildren.clear();
4270
4271 int decorationColumn = 0;
4272 const auto &attributes = e->attributes();
4273 for (auto &a : attributes) {
4274 if (a.type == QInputMethodEvent::Cursor) {
4275 newCursor = m_imPreeditRange->start() + KTextEditor::Cursor(0, a.start);
4276 hideCursor = !a.length;
4277 QColor c = qvariant_cast<QColor>(a.value);
4278 if (c.isValid()) {
4279 caretColor = c;
4280 }
4281
4282 } else if (a.type == QInputMethodEvent::TextFormat) {
4283 QTextCharFormat f = qvariant_cast<QTextFormat>(a.value).toCharFormat();
4284
4285 if (f.isValid() && decorationColumn <= a.start) {
4286 const KTextEditor::MovingCursor &preEditRangeStart = m_imPreeditRange->start();
4287 const int startLine = preEditRangeStart.line();
4288 const int startCol = preEditRangeStart.column();
4289 KTextEditor::Range fr(startLine, startCol + a.start, startLine, startCol + a.start + a.length);
4290 std::unique_ptr<KTextEditor::MovingRange> formatRange(doc()->newMovingRange(fr));
4291 KTextEditor::Attribute::Ptr attribute(new KTextEditor::Attribute());
4292 attribute->merge(f);
4293 formatRange->setAttribute(attribute);
4294 decorationColumn = a.start + a.length;
4295 m_imPreeditRangeChildren.push_back(std::move(formatRange));
4296 }
4297 }
4298 }
4299 }
4300
4301 renderer()->setDrawCaret(hideCursor);
4302 renderer()->setCaretOverrideColor(caretColor);
4303
4304 if (newCursor != m_cursor.toCursor()) {
4305 updateCursor(newCursor);
4306 }
4307
4308 e->accept();
4309 }
4310
4311 // END IM INPUT STUFF
4312
flashChar(const KTextEditor::Cursor pos,KTextEditor::Attribute::Ptr attribute)4313 void KateViewInternal::flashChar(const KTextEditor::Cursor pos, KTextEditor::Attribute::Ptr attribute)
4314 {
4315 Q_ASSERT(pos.isValid());
4316 Q_ASSERT(attribute.constData());
4317
4318 // if line is folded away, do nothing
4319 if (!view()->textFolding().isLineVisible(pos.line())) {
4320 return;
4321 }
4322
4323 KTextEditor::Range range(pos, KTextEditor::Cursor(pos.line(), pos.column() + 1));
4324 if (m_textAnimation) {
4325 m_textAnimation->deleteLater();
4326 }
4327 m_textAnimation = new KateTextAnimation(range, std::move(attribute), this);
4328 }
4329
showBracketMatchPreview()4330 void KateViewInternal::showBracketMatchPreview()
4331 {
4332 // only show when main window is active
4333 if (window() && !window()->isActiveWindow()) {
4334 return;
4335 }
4336
4337 const KTextEditor::Cursor openBracketCursor = m_bmStart->start();
4338 // make sure that the matching bracket is an opening bracket that is not visible on the current view, and that the preview won't be blocking the cursor
4339 if (m_cursor == openBracketCursor || toVirtualCursor(openBracketCursor).line() >= startLine() || m_cursor.line() - startLine() < 2) {
4340 hideBracketMatchPreview();
4341 return;
4342 }
4343
4344 if (!m_bmPreview) {
4345 m_bmPreview.reset(new KateTextPreview(m_view, this));
4346 m_bmPreview->setAttribute(Qt::WA_ShowWithoutActivating);
4347 m_bmPreview->setFrameStyle(QFrame::Box);
4348 }
4349
4350 const int previewLine = openBracketCursor.line();
4351 KateRenderer *const renderer_ = renderer();
4352 KateLineLayoutPtr lineLayout(new KateLineLayout(*renderer_));
4353 lineLayout->setLine(previewLine, -1);
4354
4355 // If the opening bracket is on its own line, start preview at the line above it instead (where the context is likely to be)
4356 const int col = lineLayout->textLine()->firstChar();
4357 if (previewLine > 0 && (col == -1 || col == openBracketCursor.column())) {
4358 lineLayout->setLine(previewLine - 1, lineLayout->virtualLine() - 1);
4359 }
4360
4361 renderer_->layoutLine(lineLayout, -1 /* no wrap */, false /* no layout cache */);
4362 const int lineWidth =
4363 qBound(m_view->width() / 5, int(lineLayout->width() + renderer_->spaceWidth() * 2), m_view->width() - m_leftBorder->width() - m_lineScroll->width());
4364 m_bmPreview->resize(lineWidth, renderer_->lineHeight() * 2);
4365 const QPoint topLeft = mapToGlobal(QPoint(0, 0));
4366 m_bmPreview->move(topLeft.x(), topLeft.y());
4367 m_bmPreview->setLine(lineLayout->virtualLine());
4368 m_bmPreview->setCenterView(false);
4369 m_bmPreview->raise();
4370 m_bmPreview->show();
4371 }
4372
hideBracketMatchPreview()4373 void KateViewInternal::hideBracketMatchPreview()
4374 {
4375 m_bmPreview.reset();
4376 }
4377
documentTextInserted(KTextEditor::Document * document,const KTextEditor::Range & range)4378 void KateViewInternal::documentTextInserted(KTextEditor::Document *document, const KTextEditor::Range &range)
4379 {
4380 #ifndef QT_NO_ACCESSIBILITY
4381 if (QAccessible::isActive()) {
4382 QAccessibleTextInsertEvent ev(this,
4383 static_cast<KateViewAccessible *>(QAccessible::queryAccessibleInterface(this))->positionFromCursor(this, range.start()),
4384 document->text(range));
4385 QAccessible::updateAccessibility(&ev);
4386 }
4387 #endif
4388 }
4389
documentTextRemoved(KTextEditor::Document *,const KTextEditor::Range & range,const QString & oldText)4390 void KateViewInternal::documentTextRemoved(KTextEditor::Document * /*document*/, const KTextEditor::Range &range, const QString &oldText)
4391 {
4392 #ifndef QT_NO_ACCESSIBILITY
4393 if (QAccessible::isActive()) {
4394 QAccessibleTextRemoveEvent ev(this,
4395 static_cast<KateViewAccessible *>(QAccessible::queryAccessibleInterface(this))->positionFromCursor(this, range.start()),
4396 oldText);
4397 QAccessible::updateAccessibility(&ev);
4398 }
4399 #endif
4400 }
4401
inlineNoteRect(const KateInlineNoteData & noteData) const4402 QRect KateViewInternal::inlineNoteRect(const KateInlineNoteData ¬eData) const
4403 {
4404 KTextEditor::InlineNote note(noteData);
4405 // compute note width and position
4406 const auto noteWidth = note.width();
4407 auto noteCursor = note.position();
4408
4409 // The cursor might be outside of the text. In that case, clamp it to the text and
4410 // later on add the missing x offset.
4411 const auto lineLength = view()->document()->lineLength(noteCursor.line());
4412 int extraOffset = -noteWidth;
4413 if (noteCursor.column() == lineLength) {
4414 extraOffset = 0;
4415 } else if (noteCursor.column() > lineLength) {
4416 extraOffset = (noteCursor.column() - lineLength) * renderer()->spaceWidth();
4417 noteCursor.setColumn(lineLength);
4418 }
4419 auto noteStartPos = mapToGlobal(cursorToCoordinate(noteCursor, true, false));
4420
4421 // compute the note's rect
4422 auto globalNoteRect = QRect(noteStartPos + QPoint{extraOffset, 0}, QSize(noteWidth, renderer()->lineHeight()));
4423
4424 return globalNoteRect;
4425 }
4426
inlineNoteAt(const QPoint & globalPos) const4427 KateInlineNoteData KateViewInternal::inlineNoteAt(const QPoint &globalPos) const
4428 {
4429 // compute the associated cursor to get the right line
4430 const int line = coordinatesToCursor(mapFromGlobal(globalPos)).line();
4431 const auto inlineNotes = view()->inlineNotes(line);
4432 // loop over all notes and check if the point is inside it
4433 for (const auto ¬e : inlineNotes) {
4434 auto globalNoteRect = inlineNoteRect(note);
4435 if (globalNoteRect.contains(globalPos)) {
4436 return note;
4437 }
4438 }
4439 // none found -- return an invalid note
4440 return {};
4441 }
4442