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

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