1/*
2 * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com)
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17#include "qconsole.h"
18
19#include <QEvent>
20#include <QInputMethodEvent>
21#include <QKeyEvent>
22#include <QPaintEvent>
23#include <QPainter>
24#include <QRect>
25#include <QScrollBar>
26#include <cmath>
27#include <QDebug>
28#include <QTimer>
29#include <QApplication>
30#include <QClipboard>
31#include "../utils.h"
32
33QConsole::QConsole(QWidget *parent):
34 QAbstractScrollArea(parent),
35 mContents(this),
36 mContentImage()
37{
38 mHistorySize = 500;
39 mHistoryIndex = -1;
40 mCommand = "";
41 mCurrentEditableLine = "";
42 mRowHeight = 0;
43 mTopRow = 1;
44 mRowsInWindow = 0;
45 mColumnsPerRow = 0;
46 mColumnWidth = 0;
47 mReadonly = false;
48 mSelectionBegin = {0,0};
49 mSelectionEnd = {0,0};
50 mCaretChar = 0;
51 mBackground = palette().color(QPalette::Base);
52 mForeground = palette().color(QPalette::Text);
53 mSelectionBackground = palette().color(QPalette::Highlight);
54 mSelectionForeground = palette().color(QPalette::HighlightedText);
55 mInactiveSelectionBackground = palette().color(QPalette::Inactive,QPalette::Highlight);
56 mInactiveSelectionForeground = palette().color(QPalette::Inactive,QPalette::HighlightedText);
57 mTabSize = 4;
58 mBlinkTimerId = 0;
59 mBlinkStatus = 0;
60 //enable input method
61 setAttribute(Qt::WA_InputMethodEnabled);
62// setMouseTracking(false);
63 recalcCharExtent();
64 mScrollTimer = new QTimer(this);
65 mScrollTimer->setInterval(100);
66 connect(mScrollTimer,&QTimer::timeout,this, &QConsole::scrollTimerHandler);
67 connect(&mContents,&ConsoleLines::layoutFinished,this, &QConsole::contentsLayouted);
68 connect(&mContents,&ConsoleLines::rowsAdded,this, &QConsole::contentsRowsAdded);
69 connect(&mContents,&ConsoleLines::lastRowsChanged,this, &QConsole::contentsLastRowsChanged);
70 connect(&mContents,&ConsoleLines::lastRowsRemoved,this, &QConsole::contentsLastRowsRemoved);
71 connect(verticalScrollBar(),&QScrollBar::valueChanged,
72 this, &QConsole::doScrolled);
73
74}
75
76int QConsole::historySize() const
77{
78 return mHistorySize;
79}
80
81void QConsole::setHistorySize(int historySize)
82{
83 mHistorySize = historySize;
84}
85
86int QConsole::tabSize() const
87{
88 return mTabSize;
89}
90
91int QConsole::columnsPerRow() const
92{
93 return mColumnsPerRow;
94}
95
96int QConsole::rowsInWindow() const
97{
98 return mRowsInWindow;
99}
100
101int QConsole::charColumns(QChar ch, int columnsBefore) const
102{
103 if (ch == '\t') {
104 return mTabSize - (columnsBefore % mTabSize);
105 }
106 if (ch == ' ')
107 return 1;
108 return std::ceil((int)(fontMetrics().horizontalAdvance(ch)) / (double) mColumnWidth);
109}
110
111void QConsole::invalidate()
112{
113 viewport()->update();
114}
115
116void QConsole::invalidateRows(int startRow, int endRow)
117{
118 if (!isVisible())
119 return;
120 if (startRow == -1 && endRow == -1) {
121 invalidate();
122 } else {
123 startRow = std::max(startRow, 1);
124 endRow = std::max(endRow, 1);
125 // find the visible lines first
126 if (startRow > endRow)
127 std::swap(startRow, endRow);
128
129 if (endRow >= mContents.rows())
130 endRow = INT_MAX; // paint empty space beyond last line
131
132 // mTopLine is in display coordinates, so FirstLine and LastLine must be
133 // converted previously.
134 startRow = std::max(startRow, mTopRow);
135 endRow = std::min(endRow, mTopRow + mRowsInWindow-1);
136
137 // any line visible?
138 if (endRow >= startRow) {
139 QRect rcInval = {
140 0,
141 mRowHeight * (startRow - mTopRow),
142 clientWidth(), mRowHeight * (endRow - mTopRow + 1)
143 };
144 invalidateRect(rcInval);
145 }
146 }
147
148}
149
150void QConsole::invalidateRect(const QRect &rect)
151{
152 viewport()->update(rect);
153}
154
155void QConsole::addLine(const QString &line)
156{
157 mCurrentEditableLine = "";
158 mCaretChar=0;
159 mSelectionBegin = caretPos();
160 mSelectionEnd = caretPos();
161 mContents.addLine(line);
162}
163
164void QConsole::addText(const QString &text)
165{
166 QStringList lst = textToLines(text);
167 for (const QString& line:lst) {
168 addLine(line);
169 }
170}
171
172void QConsole::removeLastLine()
173{
174 mCurrentEditableLine = "";
175 mCaretChar=0;
176 mSelectionBegin = caretPos();
177 mSelectionEnd = caretPos();
178 mContents.RemoveLastLine();
179}
180
181void QConsole::changeLastLine(const QString &line)
182{
183 mContents.changeLastLine(line);
184}
185
186QString QConsole::getLastLine()
187{
188 return mContents.getLastLine();
189}
190
191void QConsole::clear()
192{
193 mContents.clear();
194 mCommand = "";
195 mCurrentEditableLine = "";
196 mTopRow = 1;
197 mSelectionBegin = {0,0};
198 mSelectionEnd = {0,0};
199 mCaretChar = 0;
200 updateScrollbars();
201}
202
203void QConsole::copy()
204{
205 if (!this->hasSelection())
206 return;
207 QString s = selText();
208 QClipboard* clipboard=QGuiApplication::clipboard();
209 clipboard->clear();
210 clipboard->setText(s);
211}
212
213void QConsole::paste()
214{
215 if (mReadonly)
216 return;
217 QClipboard* clipboard=QGuiApplication::clipboard();
218 textInputed(clipboard->text());
219}
220
221void QConsole::selectAll()
222{
223 if (mContents.lines()>0) {
224 mSelectionBegin = {1,1};
225 mSelectionEnd = { mContents.getLastLine().length()+1,mContents.lines()};
226 }
227}
228
229QString QConsole::selText()
230{
231 if (!hasSelection())
232 return "";
233 int ColFrom = selectionBegin().ch;
234 int First = selectionBegin().line;
235 int ColTo = selectionEnd().ch;
236 int Last = selectionEnd().line;
237 if (First == Last) {
238 QString s = mContents.getLine(First);
239 if (First == mContents.lines()) {
240 s += this->mCommand;
241 }
242 return s.mid(ColFrom, ColTo - ColFrom);
243
244 } else {
245 QString result = mContents.getLine(First).mid(ColFrom);
246 result+= lineBreak();
247 for (int i = First + 1; i<=Last - 1; i++) {
248 result += mContents.getLine(i);
249 result+= lineBreak();
250 }
251 QString s = mContents.getLine(Last);
252 if (Last == mContents.lines())
253 s+= this->mCommand;
254 result += s.leftRef(ColTo);
255 return result;
256 }
257}
258
259void QConsole::recalcCharExtent() {
260 mRowHeight = fontMetrics().lineSpacing();
261 mColumnWidth = fontMetrics().horizontalAdvance("M");
262}
263
264void QConsole::sizeOrFontChanged(bool)
265{
266 if (mColumnWidth != 0) {
267 mColumnsPerRow = std::max(clientWidth()-2,0) / mColumnWidth;
268 mRowsInWindow = clientHeight() / mRowHeight;
269 mContents.layout();
270 }
271
272}
273
274int QConsole::clientWidth()
275{
276 return viewport()->size().width();
277}
278
279int QConsole::clientHeight()
280{
281 return viewport()->size().height();
282}
283
284void QConsole::setTopRow(int value)
285{
286 value = std::min(value,maxScrollHeight());
287 value = std::max(value, 1);
288 if (value != mTopRow) {
289 verticalScrollBar()->setValue(value);
290 }
291}
292
293int QConsole::maxScrollHeight()
294{
295 return std::max(mContents.rows()-mRowsInWindow+1,1);
296}
297
298void QConsole::updateScrollbars()
299{
300 setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
301 setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
302 int nMaxScroll = maxScrollHeight();
303 int nMin = 1;
304 int nMax = std::max(1, nMaxScroll);
305 int nPage = mRowsInWindow;
306 int nPos = std::min(std::max(mTopRow,nMin),nMax);
307 verticalScrollBar()->setMinimum(nMin);
308 verticalScrollBar()->setMaximum(nMax);
309 verticalScrollBar()->setPageStep(nPage);
310 verticalScrollBar()->setSingleStep(1);
311 if (nPos != verticalScrollBar()->value())
312 verticalScrollBar()->setValue(nPos);
313 else
314 invalidate();
315}
316
317void QConsole::paintRows(QPainter &painter, int row1, int row2)
318{
319 if (row1>row2)
320 return;
321 QRect rect(0,(row1-mTopRow)*mRowHeight,clientWidth(),(row2-row1+1)*mRowHeight);
322 painter.fillRect(rect,mBackground);
323 QStringList lst = mContents.getRows(row1,row2);
324 int startRow = row1-mTopRow;
325 painter.setPen(mForeground);
326 RowColumn selBeginRC = mContents.lineCharToRowColumn(selectionBegin());
327 RowColumn selEndRC = mContents.lineCharToRowColumn(selectionEnd());
328 LineChar editBegin = {
329 mContents.getLastLine().length() - mCurrentEditableLine.length(),
330 mContents.lines()-1
331 };
332 RowColumn editBeginRC = mContents.lineCharToRowColumn(editBegin);
333 bool isSelection = false;
334 painter.setPen(mForeground);
335 for (int i=0; i< lst.size(); i++) {
336 int currentRow = i+row1-1;
337 int left=2;
338 int top = (startRow+i) * mRowHeight;
339 int baseLine = (startRow+i+1)*mRowHeight - painter.fontMetrics().descent();
340 QString s = lst[i];
341 int columnsBefore = 0;
342 for (QChar ch:s) {
343 int charCol = charColumns(ch,columnsBefore);
344 int width = charCol * mColumnWidth;
345 if ((currentRow > selBeginRC.row ||
346 (currentRow == selBeginRC.row && columnsBefore>=selBeginRC.column))
347 &&
348 (currentRow < selEndRC.row ||
349 (currentRow == selEndRC.row && columnsBefore+charCol<=selEndRC.column))) {
350 if (!isSelection) {
351 isSelection = true;
352 }
353 if (!mReadonly &&(currentRow>editBeginRC.row ||
354 columnsBefore >= editBeginRC.column)) {
355 painter.setPen(mSelectionForeground);
356 painter.fillRect(left,top,width,mRowHeight,mSelectionBackground);
357 } else {
358 painter.setPen(mInactiveSelectionForeground);
359 painter.fillRect(left,top,width,mRowHeight,mInactiveSelectionBackground);
360 }
361 } else {
362 if (isSelection) {
363 isSelection = false;
364 painter.setPen(mForeground);
365 }
366 }
367 painter.drawText(left,baseLine,ch);
368 left+= width;
369 columnsBefore += charCol;
370 }
371 }
372}
373
374void QConsole::ensureCaretVisible()
375{
376 int caretRow = mContents.rows();
377 if (caretRow < mTopRow) {
378 mTopRow = caretRow;
379 return;
380 }
381 if (caretRow >= mTopRow + mRowsInWindow) {
382 mTopRow = caretRow + 1 -mRowsInWindow;
383 }
384}
385
386void QConsole::showCaret()
387{
388 if (mBlinkTimerId==0)
389 mBlinkTimerId = startTimer(500);
390}
391
392void QConsole::hideCaret()
393{
394 if (mBlinkTimerId!=0) {
395 killTimer(mBlinkTimerId);
396 mBlinkTimerId = 0;
397 mBlinkStatus = 0;
398 updateCaret();
399 }
400}
401
402void QConsole::updateCaret()
403{
404 QRect rcCaret = getCaretRect();
405 invalidateRect(rcCaret);
406}
407
408LineChar QConsole::caretPos()
409{
410 QString lastLine = mContents.getLastLine();
411 int line = std::max(mContents.lines()-1,0);
412 int charIndex = 0;
413 if (mCaretChar>=mCurrentEditableLine.length()) {
414 charIndex = lastLine.length();
415 } else {
416 charIndex = lastLine.length()-mCurrentEditableLine.length()+mCaretChar;
417 }
418 return {charIndex,line};
419}
420
421RowColumn QConsole::caretRowColumn()
422{
423 return mContents.lineCharToRowColumn(caretPos());
424}
425
426QPoint QConsole::rowColumnToPixels(const RowColumn &rowColumn)
427{
428 /*
429 mTopRow is 1-based; rowColumn.row is 0-based
430 */
431 int row =rowColumn.row+1 - mTopRow;
432 int col =rowColumn.column;
433 return QPoint(2+col*mColumnWidth, row*mRowHeight);
434}
435
436QRect QConsole::getCaretRect()
437{
438 LineChar caret = caretPos();
439 QChar caretChar = mContents.getChar(caret);
440 RowColumn caretRC = mContents.lineCharToRowColumn(caret);
441 QPoint caretPos = rowColumnToPixels(caretRC);
442 int caretWidth=mColumnWidth;
443 //qDebug()<<"caret"<<mCaretX<<mCaretY;
444 int columnsBefore = caretRC.column;
445 if (!caretChar.isNull()) {
446 caretWidth = charColumns(caretChar, columnsBefore)*mColumnWidth;
447 }
448 return QRect(caretPos.x(),caretPos.y(),caretWidth,
449 mRowHeight);
450}
451
452void QConsole::doScrolled()
453{
454 mTopRow = verticalScrollBar()->value();
455 invalidate();
456}
457
458void QConsole::contentsLayouted()
459{
460 updateScrollbars();
461}
462
463void QConsole::contentsRowsAdded(int )
464{
465 ensureCaretVisible();
466 updateScrollbars();
467}
468
469void QConsole::contentsLastRowsRemoved(int )
470{
471 ensureCaretVisible();
472 updateScrollbars();
473}
474
475void QConsole::contentsLastRowsChanged(int rowCount)
476{
477 ensureCaretVisible();
478 invalidateRows(mContents.rows()-rowCount+1,mContents.rows());
479}
480
481void QConsole::scrollTimerHandler()
482
483{
484 QPoint iMousePos;
485
486 iMousePos = QCursor::pos();
487 iMousePos = mapFromGlobal(iMousePos);
488 RowColumn mousePosRC = pixelsToNearestRowColumn(iMousePos.x(),iMousePos.y());
489
490 if (mScrollDeltaY != 0) {
491 qDebug()<<"scroll timer"<<mScrollDeltaY;
492 qDebug()<<mousePosRC.row<<mousePosRC.column;
493 if (QApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier))
494 setTopRow(mTopRow + mScrollDeltaY * mRowsInWindow);
495 else
496 setTopRow(mTopRow + mScrollDeltaY);
497 int row = mTopRow;
498 if (mScrollDeltaY > 0) // scrolling down?
499 row+=mRowsInWindow - 1;
500 mousePosRC.row = row - 1;
501 qDebug()<<row;
502 int oldStartRow = mContents.lineCharToRowColumn(selectionBegin()).row+1;
503 int oldEndRow = mContents.lineCharToRowColumn(selectionEnd()).row+1;
504 invalidateRows(oldStartRow,oldEndRow);
505 mSelectionEnd = mContents.rowColumnToLineChar(mousePosRC);
506 invalidateRows(row,row);
507 }
508
509// computeScrollY(Y);
510}
511
512void QConsole::mousePressEvent(QMouseEvent *event)
513{
514 Qt::MouseButton button = event->button();
515 int X=event->pos().x();
516 int Y=event->pos().y();
517
518 QAbstractScrollArea::mousePressEvent(event);
519
520 //fKbdHandler.ExecuteMouseDown(Self, Button, Shift, X, Y);
521
522 if (button == Qt::LeftButton) {
523// setMouseTracking(true);
524 RowColumn mousePosRC = pixelsToNearestRowColumn(X,Y);
525 LineChar mousePos = mContents.rowColumnToLineChar(mousePosRC);
526 //I couldn't track down why, but sometimes (and definitely not all the time)
527 //the block positioning is lost. This makes sure that the block is
528 //maintained in case they started a drag operation on the block
529 int oldStartRow = mContents.lineCharToRowColumn(selectionBegin()).row+1;
530 int oldEndRow = mContents.lineCharToRowColumn(selectionEnd()).row+1;
531 invalidateRows(oldStartRow,oldEndRow);
532 mSelectionBegin = mousePos;
533 mSelectionEnd = mousePos;
534 }
535}
536
537void QConsole::mouseReleaseEvent(QMouseEvent *event)
538{
539 QAbstractScrollArea::mouseReleaseEvent(event);
540 mScrollTimer->stop();
541// setMouseTracking(false);
542
543}
544
545void QConsole::mouseMoveEvent(QMouseEvent *event)
546{
547 QAbstractScrollArea::mouseMoveEvent(event);
548 Qt::MouseButtons buttons = event->buttons();
549 int X=event->pos().x();
550 int Y=event->pos().y();
551
552 if ((buttons == Qt::LeftButton)) {
553 // should we begin scrolling?
554 computeScrollY(Y);
555 RowColumn mousePosRC = pixelsToNearestRowColumn(X, Y);
556 LineChar mousePos = mContents.rowColumnToLineChar(mousePosRC);
557 if (mScrollDeltaY == 0) {
558 int oldStartRow = mContents.lineCharToRowColumn(selectionBegin()).row+1;
559 int oldEndRow = mContents.lineCharToRowColumn(selectionEnd()).row+1;
560 invalidateRows(oldStartRow,oldEndRow);
561 mSelectionEnd = mousePos;
562 int row = mContents.lineCharToRowColumn(mSelectionEnd).row+1;
563 invalidateRows(row,row);
564 }
565 }
566}
567
568void QConsole::keyPressEvent(QKeyEvent *event)
569{
570 switch(event->key()) {
571 case Qt::Key_Return:
572 case Qt::Key_Enter:
573 event->accept();
574 if (mReadonly)
575 return;
576 emit commandInput(mCommand);
577 if (mHistorySize>0) {
578 if (mCommandHistory.size()==mHistorySize) {
579 mCommandHistory.pop_front();
580 mHistoryIndex--;
581 }
582 if (mCommandHistory.size()==0 || mHistoryIndex<0 || mHistoryIndex == mCommandHistory.size()-1) {
583 mHistoryIndex=mCommandHistory.size();
584 }
585 mCommandHistory.append(mCommand);
586 }
587 mCommand="";
588 addLine("");
589 return;
590 case Qt::Key_Up:
591 event->accept();
592 if (mHistorySize>0 && mHistoryIndex>0) {
593 mHistoryIndex--;
594 loadCommandFromHistory();
595 }
596 return;
597 case Qt::Key_Down:
598 event->accept();
599 if (mHistorySize>0 && mHistoryIndex<mCommandHistory.size()-1) {
600 mHistoryIndex++;
601 loadCommandFromHistory();
602 }
603 return;
604 case Qt::Key_Left:
605 event->accept();
606 if (mReadonly)
607 return;
608 if (mCaretChar>0 && mCaretChar<=mCurrentEditableLine.size()) {
609 setCaretChar(mCaretChar-1, !(event->modifiers() & Qt::ShiftModifier));
610 }
611 return;
612 case Qt::Key_Right:
613 event->accept();
614 if (mReadonly)
615 return;
616 if (mCaretChar<mCurrentEditableLine.size()) {
617 setCaretChar(mCaretChar+1, !(event->modifiers() & Qt::ShiftModifier));
618 }
619 return;
620 case Qt::Key_Home:
621 event->accept();
622 if (mReadonly)
623 return;
624 if (mCaretChar>0 && mCaretChar<=mCurrentEditableLine.size()) {
625 setCaretChar(0, !(event->modifiers() & Qt::ShiftModifier));
626 }
627 return;
628 case Qt::Key_End:
629 event->accept();
630 if (mReadonly)
631 return;
632 if (mCaretChar<mCurrentEditableLine.size()) {
633 setCaretChar(mCurrentEditableLine.size(), !(event->modifiers() & Qt::ShiftModifier));
634 }
635 return;
636 case Qt::Key_Backspace:
637 event->accept();
638 if (mReadonly)
639 return;
640 if (!mCurrentEditableLine.isEmpty() && (mCaretChar-1)<mCurrentEditableLine.size()
641 &&(mCaretChar-1>=0)) {
642 QString lastLine;
643 if (caretInSelection()) {
644 lastLine = removeSelection();
645 } else {
646 lastLine = mContents.getLastLine();
647 int len=mCurrentEditableLine.length();
648 mCaretChar--;
649 mCommand.remove(mCommand.length()-len+mCaretChar,1);
650 lastLine.remove(lastLine.length()-len+mCaretChar,1);
651 mCurrentEditableLine.remove(mCaretChar,1);
652 }
653 mContents.changeLastLine(lastLine);
654 mSelectionBegin = caretPos();
655 mSelectionEnd = caretPos();
656 } else {
657 if (hasSelection()) {
658 mSelectionBegin = caretPos();
659 mSelectionEnd = caretPos();
660 invalidate();
661 }
662 }
663 return;
664 case Qt::Key_Delete:
665 event->accept();
666 if (mReadonly)
667 return;
668 if (!mCurrentEditableLine.isEmpty() && (mCaretChar)<mCurrentEditableLine.size()
669 &&(mCaretChar>=0)) {
670 QString lastLine;
671 if (caretInSelection()) {
672 lastLine = removeSelection();
673 } else {
674 lastLine = mContents.getLastLine();
675 int len=mCurrentEditableLine.length();
676 mCommand.remove(mCommand.length()-len+mCaretChar,1);
677 lastLine.remove(lastLine.length()-len+mCaretChar,1);
678 mCurrentEditableLine.remove(mCaretChar,1);
679 }
680 mContents.changeLastLine(lastLine);
681 mSelectionBegin = caretPos();
682 mSelectionEnd = caretPos();
683 } else {
684 if (hasSelection()) {
685 mSelectionBegin = caretPos();
686 mSelectionEnd = caretPos();
687 invalidate();
688 }
689 }
690 return;
691 default:
692 if (!event->text().isEmpty()) {
693 event->accept();
694 if (mReadonly)
695 return;
696 textInputed(event->text());
697 return;
698 }
699 }
700 QAbstractScrollArea::keyPressEvent(event);
701}
702
703void QConsole::focusInEvent(QFocusEvent *)
704{
705 showCaret();
706}
707
708void QConsole::focusOutEvent(QFocusEvent *)
709{
710 hideCaret();
711}
712
713void QConsole::paintEvent(QPaintEvent *event)
714{
715 if (mRowHeight==0)
716 return;
717 // Now paint everything while the caret is hidden.
718 QPainter painter(viewport());
719 //Get the invalidated rect.
720 QRect rcClip = event->rect();
721 QRect rcCaret= getCaretRect();
722
723 if (rcCaret == rcClip) {
724 // only update caret
725 painter.drawImage(rcCaret,*mContentImage,rcCaret);
726 } else {
727 int nL1, nL2;
728 // Compute the invalid area in lines
729 // lines
730 nL1 = std::min(std::max(mTopRow + rcClip.top() / mRowHeight, mTopRow), maxScrollHeight() + mRowsInWindow - 1 );
731 nL2 = std::min(std::max(mTopRow + (rcClip.bottom() + mRowHeight - 1) / mRowHeight, 1), maxScrollHeight() + mRowsInWindow - 1);
732 QPainter cachePainter(mContentImage.get());
733 cachePainter.setFont(font());
734 if (viewport()->rect() == rcClip) {
735 painter.fillRect(rcClip, mBackground);
736 }
737 paintRows(cachePainter,nL1,nL2);
738 painter.drawImage(rcClip,*mContentImage,rcClip);
739 }
740 paintCaret(painter, rcCaret);
741}
742
743void QConsole::paintCaret(QPainter &painter, const QRect rcClip)
744{
745 if (mBlinkStatus!=1)
746 return;
747 painter.setClipRect(rcClip);
748 ConsoleCaretType ct = ConsoleCaretType::ctHorizontalLine;
749 QColor caretColor = mForeground;
750 switch(ct) {
751 case ConsoleCaretType::ctVerticalLine:
752 painter.fillRect(rcClip.left()+1,rcClip.top(),rcClip.left()+2,rcClip.bottom(),caretColor);
753 break;
754 case ConsoleCaretType::ctHorizontalLine:
755 painter.fillRect(rcClip.left(),rcClip.bottom()-2,rcClip.right(),rcClip.bottom()-1,caretColor);
756 break;
757 case ConsoleCaretType::ctBlock:
758 painter.fillRect(rcClip, caretColor);
759 break;
760 case ConsoleCaretType::ctHalfBlock:
761 QRect rc=rcClip;
762 rc.setTop(rcClip.top()+rcClip.height() / 2);
763 painter.fillRect(rcClip, caretColor);
764 break;
765 }
766}
767
768void QConsole::textInputed(const QString &text)
769{
770 if (mContents.rows()<=0) {
771 mContents.addLine("");
772 }
773 QString lastLine;
774 if (caretInSelection()) {
775 lastLine = removeSelection();
776 } else {
777 lastLine = mContents.getLastLine();
778 }
779 if (mCaretChar>=mCurrentEditableLine.size()) {
780 mCommand += text;
781 mCurrentEditableLine += text;
782 mCaretChar=mCurrentEditableLine.size();
783 mContents.changeLastLine(lastLine+text);
784 } else {
785 int len=mCurrentEditableLine.length();
786 mCommand.insert(mCommand.length()-len+mCaretChar,text);
787 lastLine.insert(lastLine.length()-len+mCaretChar,text);
788 mCurrentEditableLine.insert(mCaretChar,text);
789 mContents.changeLastLine(lastLine);
790 mCaretChar+=text.length();
791 }
792 mSelectionBegin = caretPos();
793 mSelectionEnd = caretPos();
794}
795
796void QConsole::loadCommandFromHistory()
797{
798 if (mHistorySize<=0)
799 return;
800 mCommand = mCommandHistory[mHistoryIndex];
801 QString lastLine = mContents.getLastLine();
802 int len=mCurrentEditableLine.length();
803 lastLine.remove(lastLine.length()-len,INT_MAX);
804 mCurrentEditableLine=mCommand;
805 mCaretChar = mCurrentEditableLine.length();
806 mSelectionBegin = caretPos();
807 mSelectionEnd = caretPos();
808 mContents.changeLastLine(lastLine + mCommand);
809}
810
811LineChar QConsole::selectionBegin()
812{
813 if (mSelectionBegin.line < mSelectionEnd.line ||
814 (mSelectionBegin.line == mSelectionEnd.line &&
815 mSelectionBegin.ch < mSelectionEnd.ch))
816 return mSelectionBegin;
817 return mSelectionEnd;
818}
819
820LineChar QConsole::selectionEnd()
821{
822 if (mSelectionBegin.line < mSelectionEnd.line ||
823 (mSelectionBegin.line == mSelectionEnd.line &&
824 mSelectionBegin.ch < mSelectionEnd.ch))
825 return mSelectionEnd;
826 return mSelectionBegin;
827}
828
829void QConsole::setCaretChar(int newCaretChar, bool resetSelection)
830{
831 RowColumn oldPosRC = caretRowColumn();
832 RowColumn oldSelBegin = mContents.lineCharToRowColumn(selectionBegin());
833 RowColumn oldSelEnd = mContents.lineCharToRowColumn(selectionEnd());
834 int oldStartRow = std::min(std::min(oldPosRC.row,oldSelBegin.row),oldSelEnd.row);
835 int oldEndRow = std::max(std::max(oldPosRC.row,oldSelBegin.row),oldSelEnd.row);
836 mCaretChar = newCaretChar;
837 LineChar newPos = caretPos();
838 RowColumn newPosRC = mContents.lineCharToRowColumn(newPos);
839 if (resetSelection)
840 mSelectionBegin = newPos;
841 mSelectionEnd = newPos;
842
843 int startRow = std::min(newPosRC.row, oldStartRow)+1;
844 int endRow = std::max(newPosRC.row, oldEndRow)+1;
845 invalidateRows(startRow, endRow);
846}
847
848bool QConsole::caretInSelection()
849{
850 if (!hasSelection())
851 return false;
852 //LineChar selBegin = selectionBegin();
853 LineChar selEnd = selectionEnd();
854 QString lastline = mContents.getLastLine();
855 int editBeginChar = lastline.length() - mCurrentEditableLine.length();
856 if (selEnd.line == mContents.lines()-1 && selEnd.ch > editBeginChar ) {
857 return true;
858 }
859 return false;
860}
861
862QString QConsole::removeSelection()
863{
864
865 QString lastLine = mContents.getLastLine();
866 int len=mCurrentEditableLine.length();
867 LineChar selBegin = selectionBegin();
868 LineChar selEnd = selectionEnd();
869 int selLen = selEnd.ch -selBegin.ch;
870 int ch = selBegin.ch -(lastLine.length()-len);
871 if (selBegin.line < mContents.lines()-1) {
872 mCaretChar = 0;
873 selLen = selEnd.ch - (lastLine.length()-len);
874 } else if (ch<0) {
875 mCaretChar = 0;
876 selLen = selLen + ch;
877 } else {
878 mCaretChar=ch;
879 }
880 mCommand.remove(mCommand.length()-len+mCaretChar,selLen);
881 lastLine.remove(lastLine.length()-len+mCaretChar,selLen);
882 mCurrentEditableLine.remove(mCaretChar,selLen);
883 return lastLine;
884}
885
886bool QConsole::hasSelection()
887{
888 return (mSelectionBegin.line != mSelectionEnd.line)
889 || (mSelectionBegin.ch != mSelectionEnd.ch);
890}
891
892int QConsole::computeScrollY(int Y)
893{
894 QRect iScrollBounds = viewport()->rect();
895 if (Y < iScrollBounds.top())
896 mScrollDeltaY = (Y - iScrollBounds.top()) / mRowHeight - 1;
897 else if (Y >= iScrollBounds.bottom())
898 mScrollDeltaY = (Y - iScrollBounds.bottom()) / mRowHeight + 1;
899 else
900 mScrollDeltaY = 0;
901
902 if (mScrollDeltaY)
903 mScrollTimer->start();
904 return mScrollDeltaY;
905}
906
907RowColumn QConsole::pixelsToNearestRowColumn(int x, int y)
908{
909 // Result is in display coordinates
910 // don't return a partially visible last line
911 if (y >= mRowsInWindow * mRowHeight) {
912 y = mRowsInWindow * mRowHeight - 1;
913 if (y < 0)
914 y = 0;
915 }
916 return {
917 std::max(0, (x - 2) / mColumnWidth),
918 mTopRow + (y / mRowHeight)-1
919 };
920}
921
922QString QConsole::lineBreak()
923{
924 return "\r\n";
925}
926
927
928void QConsole::fontChanged()
929{
930 recalcCharExtent();
931 sizeOrFontChanged(true);
932}
933
934bool QConsole::event(QEvent *event)
935{
936 switch(event->type()) {
937 case QEvent::FontChange:
938 fontChanged();
939 break;
940 case QEvent::PaletteChange:
941 mBackground = palette().color(QPalette::Base);
942 mForeground = palette().color(QPalette::Text);
943 mSelectionBackground = palette().color(QPalette::Highlight);
944 mSelectionForeground = palette().color(QPalette::HighlightedText);
945 mInactiveSelectionBackground = palette().color(QPalette::Inactive,QPalette::Highlight);
946 mInactiveSelectionForeground = palette().color(QPalette::Inactive,QPalette::HighlightedText);
947 break;
948 default:
949 break;
950 }
951 return QAbstractScrollArea::event(event);
952}
953
954void QConsole::resizeEvent(QResizeEvent *)
955{
956 //resize the cache image
957 std::shared_ptr<QImage> image = std::make_shared<QImage>(clientWidth(),clientHeight(),
958 QImage::Format_ARGB32);
959 if (mContentImage) {
960 //QRect newRect = image->rect().intersected(mContentImage->rect());
961 QPainter painter(image.get());
962 painter.fillRect(viewport()->rect(),mBackground);
963// painter.drawImage(newRect,*mContentImage);
964 }
965
966 mContentImage = image;
967
968 sizeOrFontChanged(false);
969}
970
971void QConsole::timerEvent(QTimerEvent *event)
972{
973 if (event->timerId() == mBlinkTimerId) {
974 mBlinkStatus = 1- mBlinkStatus;
975 updateCaret();
976 }
977}
978
979void QConsole::inputMethodEvent(QInputMethodEvent *event)
980{
981 if (mReadonly)
982 return;
983 QString s=event->commitString();
984 if (!s.isEmpty())
985 textInputed(s);
986}
987
988void QConsole::wheelEvent(QWheelEvent *event)
989{
990 if (event->angleDelta().y()>0) {
991 verticalScrollBar()->setValue(verticalScrollBar()->value()-1);
992 event->accept();
993 return;
994 } else if (event->angleDelta().y()<0) {
995 verticalScrollBar()->setValue(verticalScrollBar()->value()+1);
996 event->accept();
997 return;
998 }
999}
1000
1001int ConsoleLines::rows() const
1002{
1003 return mRows;
1004}
1005
1006int ConsoleLines::lines() const
1007{
1008 return mLines.count();
1009}
1010
1011void ConsoleLines::layout()
1012{
1013 if (!mConsole)
1014 return;
1015 if (mLayouting) {
1016 mNeedRelayout = true;
1017 return;
1018 }
1019 mLayouting = true;
1020 mNeedRelayout = false;
1021 emit layoutStarted();
1022 mRows = 0;
1023 bool forceUpdate = (mOldTabSize!=mConsole->tabSize());
1024 for (PConsoleLine consoleLine: mLines) {
1025 if (forceUpdate || consoleLine->maxColumns > mConsole->columnsPerRow()) {
1026 consoleLine->maxColumns = breakLine(consoleLine->text,consoleLine->fragments);
1027 }
1028 mRows+=consoleLine->fragments.count();
1029 }
1030 emit layoutFinished();
1031 mLayouting = false;
1032 if (mNeedRelayout)
1033 emit needRelayout();
1034}
1035
1036ConsoleLines::ConsoleLines(QConsole *console)
1037{
1038 mConsole = console;
1039 mRows = 0;
1040 mLayouting = false;
1041 mNeedRelayout = false;
1042 mOldTabSize = -1;
1043 mMaxLines = 1000;
1044 connect(this,&ConsoleLines::needRelayout,this,&ConsoleLines::layout);
1045}
1046
1047void ConsoleLines::addLine(const QString &line)
1048{
1049 PConsoleLine consoleLine=std::make_shared<ConsoleLine>();
1050 consoleLine->text = line;
1051 consoleLine->maxColumns = breakLine(line,consoleLine->fragments);
1052 if (mLines.count()<mMaxLines || mMaxLines <= 0) {
1053 mLines.append(consoleLine);
1054 mRows += consoleLine->fragments.count();
1055 emit rowsAdded(consoleLine->fragments.count());
1056 } else {
1057 PConsoleLine firstLine = mLines[0];
1058 mLines.pop_front();
1059 mRows -= firstLine->fragments.count();
1060 mLines.append(consoleLine);
1061 mRows += consoleLine->fragments.count();
1062 emit layoutStarted();
1063 emit layoutFinished();
1064 }
1065}
1066
1067void ConsoleLines::RemoveLastLine()
1068{
1069 if (mLines.count()<=0)
1070 return;
1071 PConsoleLine consoleLine = mLines[mLines.count()-1];
1072 mLines.pop_back();
1073 mRows -= consoleLine->fragments.count();
1074 emit lastRowsRemoved(consoleLine->fragments.count());
1075}
1076
1077void ConsoleLines::changeLastLine(const QString &newLine)
1078{
1079 if (mLines.count()<=0) {
1080 return;
1081 }
1082 PConsoleLine consoleLine = mLines[mLines.count()-1];
1083 int oldRows = consoleLine->fragments.count();
1084 consoleLine->text = newLine;
1085 breakLine(newLine,consoleLine->fragments);
1086 int newRows = consoleLine->fragments.count();
1087 if (newRows == oldRows) {
1088 emit lastRowsChanged(oldRows);
1089 return ;
1090 } else {
1091 mRows -= oldRows;
1092 mRows += newRows;
1093 emit layoutStarted();
1094 emit layoutFinished();
1095 }
1096}
1097
1098QString ConsoleLines::getLastLine()
1099{
1100 if (mLines.count()<=0)
1101 return "";
1102 return mLines[mLines.count()-1]->text;
1103}
1104
1105QString ConsoleLines::getLine(int line)
1106{
1107 if (line>=0 && line < mLines.count()) {
1108 return mLines[line]->text;
1109 }
1110 return "";
1111}
1112
1113QChar ConsoleLines::getChar(int line, int ch)
1114{
1115 QString s = getLine(line);
1116 if (ch>=0 && ch<s.length()) {
1117 return s[ch];
1118 } else {
1119 return QChar();
1120 }
1121}
1122
1123QChar ConsoleLines::getChar(const LineChar &lineChar)
1124{
1125 return getChar(lineChar.line,lineChar.ch);
1126}
1127
1128QStringList ConsoleLines::getRows(int startRow, int endRow)
1129{
1130 if (startRow>mRows)
1131 return QStringList();
1132 if (startRow > endRow)
1133 return QStringList();
1134 QStringList lst;
1135 int row = 0;
1136 for (PConsoleLine line:mLines) {
1137 for (const QString& s:line->fragments) {
1138 row+=1;
1139 if (row>endRow) {
1140 return lst;
1141 }
1142 if (row>=startRow) {
1143 lst.append(s);
1144 }
1145 }
1146 }
1147 return lst;
1148}
1149
1150LineChar ConsoleLines::rowColumnToLineChar(const RowColumn &rowColumn)
1151{
1152 return rowColumnToLineChar(rowColumn.row,rowColumn.column);
1153}
1154
1155LineChar ConsoleLines::rowColumnToLineChar(int row, int column)
1156{
1157 LineChar result{column,mLines.size()-1};
1158 int rows=0;
1159 for (int i=0;i<mLines.size();i++) {
1160 PConsoleLine line = mLines[i];
1161 if (row >= rows && row<rows+line->fragments.size()) {
1162 int r=row - rows;
1163 QString fragment = line->fragments[r];
1164 int columnsBefore = 0;
1165 for (int j=0;j<fragment.size();j++) {
1166 QChar ch = fragment[j];
1167 int charColumns= mConsole->charColumns(ch, columnsBefore);
1168 if (column>=columnsBefore && column<columnsBefore+charColumns) {
1169 result.ch = j;
1170 break;
1171 }
1172 }
1173 result.line = i;
1174 break;
1175 }
1176 rows += line->fragments.size();
1177 }
1178 return result;
1179}
1180
1181RowColumn ConsoleLines::lineCharToRowColumn(const LineChar &lineChar)
1182{
1183 return lineCharToRowColumn(lineChar.line,lineChar.ch);
1184}
1185
1186RowColumn ConsoleLines::lineCharToRowColumn(int line, int ch)
1187{
1188 RowColumn result{ch,std::max(0,mRows-1)};
1189 int rowsBefore = 0;
1190 if (line>=0 && line < mLines.size()) {
1191 for (int i=0;i<line;i++) {
1192 int rows = mLines[i]->fragments.size();
1193 rowsBefore += rows;
1194 }
1195 PConsoleLine consoleLine = mLines[line];
1196 int charsBefore = 0;
1197 for (int r=0;r<consoleLine->fragments.size();r++) {
1198 int chars = consoleLine->fragments[r].size();
1199 if (r==consoleLine->fragments.size()-1 || (ch>=charsBefore && ch<charsBefore+chars)) {
1200 QString fragment = consoleLine->fragments[r];
1201 int columnsBefore = 0;
1202 int len = std::min(ch-charsBefore,fragment.size());
1203 for (int j=0;j<len;j++) {
1204 QChar ch = fragment[j];
1205 int charColumns = mConsole->charColumns(ch,columnsBefore);
1206 columnsBefore += charColumns;
1207 }
1208 result.column=columnsBefore;
1209 result.row = rowsBefore + r;
1210 break;
1211 }
1212 charsBefore += chars;
1213 }
1214 }
1215 return result;
1216}
1217
1218bool ConsoleLines::layouting() const
1219{
1220 return mLayouting;
1221}
1222
1223int ConsoleLines::breakLine(const QString &line, QStringList &fragments)
1224{
1225 fragments.clear();
1226 QString s = "";
1227 int maxColLen = 0;
1228 int columnsBefore = 0;
1229 for (QChar ch:line) {
1230 int charColumn = mConsole->charColumns(ch,columnsBefore);
1231 if (charColumn + columnsBefore > mConsole->columnsPerRow()) {
1232 if (ch == '\t') {
1233 if (columnsBefore != mConsole->columnsPerRow()) {
1234 charColumn = 0;
1235 } else
1236 charColumn = mConsole->tabSize();
1237 }
1238 fragments.append(s);
1239 if (columnsBefore > maxColLen) {
1240 maxColLen = columnsBefore;
1241 }
1242 s = "";
1243 columnsBefore = 0;
1244 }
1245 if (charColumn > 0) {
1246 columnsBefore += charColumn;
1247 s += ch;
1248 }
1249 }
1250 if (fragments.count() == 0 || !s.isEmpty()) {
1251 fragments.append(s);
1252 if (columnsBefore > maxColLen) {
1253 maxColLen = columnsBefore;
1254 }
1255 }
1256 return maxColLen;
1257}
1258
1259int ConsoleLines::getMaxLines() const
1260{
1261 return mMaxLines;
1262}
1263
1264int ConsoleLines::maxLines() const
1265{
1266 return mMaxLines;
1267}
1268
1269void ConsoleLines::setMaxLines(int maxLines)
1270{
1271 mMaxLines = maxLines;
1272 if (mMaxLines > 0) {
1273 while (mLines.count()>mMaxLines) {
1274 mLines.pop_front();
1275 }
1276 }
1277}
1278
1279void ConsoleLines::clear()
1280{
1281 mLines.clear();
1282 mRows = 0;
1283}
1284