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

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