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