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