1267 lines
35 KiB
C++
1267 lines
35 KiB
C++
#include "qconsole.h"
|
|
|
|
#include <QEvent>
|
|
#include <QInputMethodEvent>
|
|
#include <QKeyEvent>
|
|
#include <QPaintEvent>
|
|
#include <QPainter>
|
|
#include <QRect>
|
|
#include <QScrollBar>
|
|
#include <cmath>
|
|
#include <QDebug>
|
|
#include <QTimer>
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include "../utils.h"
|
|
|
|
QConsole::QConsole(QWidget *parent):
|
|
QAbstractScrollArea(parent),
|
|
mContents(this),
|
|
mContentImage()
|
|
{
|
|
mHistorySize = 500;
|
|
mHistoryIndex = -1;
|
|
mCommand = "";
|
|
mCurrentEditableLine = "";
|
|
mRowHeight = 0;
|
|
mTopRow = 1;
|
|
mRowsInWindow = 0;
|
|
mColumnsPerRow = 0;
|
|
mColumnWidth = 0;
|
|
mReadonly = false;
|
|
mSelectionBegin = {0,0};
|
|
mSelectionEnd = {0,0};
|
|
mCaretChar = 0;
|
|
mBackground = palette().color(QPalette::Base);
|
|
mForeground = palette().color(QPalette::Text);
|
|
mSelectionBackground = palette().color(QPalette::Highlight);
|
|
mSelectionForeground = palette().color(QPalette::HighlightedText);
|
|
mInactiveSelectionBackground = palette().color(QPalette::Inactive,QPalette::Highlight);
|
|
mInactiveSelectionForeground = palette().color(QPalette::Inactive,QPalette::HighlightedText);
|
|
mTabSize = 4;
|
|
mBlinkTimerId = 0;
|
|
mBlinkStatus = 0;
|
|
//enable input method
|
|
setAttribute(Qt::WA_InputMethodEnabled);
|
|
setMouseTracking(false);
|
|
recalcCharExtent();
|
|
mScrollTimer = new QTimer(this);
|
|
mScrollTimer->setInterval(100);
|
|
connect(mScrollTimer,&QTimer::timeout,this, &QConsole::scrollTimerHandler);
|
|
connect(&mContents,&ConsoleLines::layoutFinished,this, &QConsole::contentsLayouted);
|
|
connect(&mContents,&ConsoleLines::rowsAdded,this, &QConsole::contentsRowsAdded);
|
|
connect(&mContents,&ConsoleLines::lastRowsChanged,this, &QConsole::contentsLastRowsChanged);
|
|
connect(&mContents,&ConsoleLines::lastRowsRemoved,this, &QConsole::contentsLastRowsRemoved);
|
|
connect(verticalScrollBar(),&QScrollBar::valueChanged,
|
|
this, &QConsole::doScrolled);
|
|
|
|
}
|
|
|
|
int QConsole::historySize() const
|
|
{
|
|
return mHistorySize;
|
|
}
|
|
|
|
void QConsole::setHistorySize(int historySize)
|
|
{
|
|
mHistorySize = historySize;
|
|
}
|
|
|
|
int QConsole::tabSize() const
|
|
{
|
|
return mTabSize;
|
|
}
|
|
|
|
int QConsole::columnsPerRow() const
|
|
{
|
|
return mColumnsPerRow;
|
|
}
|
|
|
|
int QConsole::rowsInWindow() const
|
|
{
|
|
return mRowsInWindow;
|
|
}
|
|
|
|
int QConsole::charColumns(QChar ch, int columnsBefore) const
|
|
{
|
|
if (ch == '\t') {
|
|
return mTabSize - (columnsBefore % mTabSize);
|
|
}
|
|
if (ch == ' ')
|
|
return 1;
|
|
return std::ceil((int)(fontMetrics().horizontalAdvance(ch)) / (double) mColumnWidth);
|
|
}
|
|
|
|
void QConsole::invalidate()
|
|
{
|
|
viewport()->update();
|
|
}
|
|
|
|
void QConsole::invalidateRows(int startRow, int endRow)
|
|
{
|
|
if (!isVisible())
|
|
return;
|
|
if (startRow == -1 && endRow == -1) {
|
|
invalidate();
|
|
} else {
|
|
startRow = std::max(startRow, 1);
|
|
endRow = std::max(endRow, 1);
|
|
// find the visible lines first
|
|
if (startRow > endRow)
|
|
std::swap(startRow, endRow);
|
|
|
|
if (endRow >= mContents.rows())
|
|
endRow = INT_MAX; // paint empty space beyond last line
|
|
|
|
// mTopLine is in display coordinates, so FirstLine and LastLine must be
|
|
// converted previously.
|
|
startRow = std::max(startRow, mTopRow);
|
|
endRow = std::min(endRow, mTopRow + mRowsInWindow-1);
|
|
|
|
// any line visible?
|
|
if (endRow >= startRow) {
|
|
QRect rcInval = {
|
|
0,
|
|
mRowHeight * (startRow - mTopRow),
|
|
clientWidth(), mRowHeight * (endRow - mTopRow + 1)
|
|
};
|
|
invalidateRect(rcInval);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void QConsole::invalidateRect(const QRect &rect)
|
|
{
|
|
viewport()->update(rect);
|
|
}
|
|
|
|
void QConsole::addLine(const QString &line)
|
|
{
|
|
mCurrentEditableLine = "";
|
|
mCaretChar=0;
|
|
mSelectionBegin = caretPos();
|
|
mSelectionEnd = caretPos();
|
|
mContents.addLine(line);
|
|
}
|
|
|
|
void QConsole::addText(const QString &text)
|
|
{
|
|
QStringList lst = TextToLines(text);
|
|
for (const QString& line:lst) {
|
|
addLine(line);
|
|
}
|
|
}
|
|
|
|
void QConsole::removeLastLine()
|
|
{
|
|
mCurrentEditableLine = "";
|
|
mCaretChar=0;
|
|
mSelectionBegin = caretPos();
|
|
mSelectionEnd = caretPos();
|
|
mContents.RemoveLastLine();
|
|
}
|
|
|
|
void QConsole::changeLastLine(const QString &line)
|
|
{
|
|
mContents.changeLastLine(line);
|
|
}
|
|
|
|
QString QConsole::getLastLine()
|
|
{
|
|
return mContents.getLastLine();
|
|
}
|
|
|
|
void QConsole::clear()
|
|
{
|
|
mContents.clear();
|
|
mCommand = "";
|
|
mCurrentEditableLine = "";
|
|
mTopRow = 1;
|
|
mSelectionBegin = {0,0};
|
|
mSelectionEnd = {0,0};
|
|
mCaretChar = 0;
|
|
updateScrollbars();
|
|
}
|
|
|
|
void QConsole::copy()
|
|
{
|
|
if (!this->hasSelection())
|
|
return;
|
|
QString s = selText();
|
|
QClipboard* clipboard=QGuiApplication::clipboard();
|
|
clipboard->clear();
|
|
clipboard->setText(s);
|
|
}
|
|
|
|
void QConsole::paste()
|
|
{
|
|
if (mReadonly)
|
|
return;
|
|
QClipboard* clipboard=QGuiApplication::clipboard();
|
|
textInputed(clipboard->text());
|
|
}
|
|
|
|
void QConsole::selectAll()
|
|
{
|
|
if (mContents.lines()>0) {
|
|
mSelectionBegin = {1,1};
|
|
mSelectionEnd = { mContents.getLastLine().length()+1,mContents.lines()};
|
|
}
|
|
}
|
|
|
|
QString QConsole::selText()
|
|
{
|
|
if (!hasSelection())
|
|
return "";
|
|
int ColFrom = selectionBegin().ch;
|
|
int First = selectionBegin().line;
|
|
int ColTo = selectionEnd().ch;
|
|
int Last = selectionEnd().line;
|
|
if (First == Last) {
|
|
QString s = mContents.getLine(First);
|
|
if (First == mContents.lines()) {
|
|
s += this->mCommand;
|
|
}
|
|
return s.mid(ColFrom, ColTo - ColFrom);
|
|
|
|
} else {
|
|
QString result = mContents.getLine(First).mid(ColFrom);
|
|
result+= lineBreak();
|
|
for (int i = First + 1; i<=Last - 1; i++) {
|
|
result += mContents.getLine(i);
|
|
result+= lineBreak();
|
|
}
|
|
QString s = mContents.getLine(Last);
|
|
if (Last == mContents.lines())
|
|
s+= this->mCommand;
|
|
result += s.leftRef(ColTo);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
void QConsole::recalcCharExtent() {
|
|
mRowHeight = fontMetrics().lineSpacing();
|
|
mColumnWidth = fontMetrics().horizontalAdvance("M");
|
|
}
|
|
|
|
void QConsole::sizeOrFontChanged(bool)
|
|
{
|
|
if (mColumnWidth != 0) {
|
|
mColumnsPerRow = std::max(clientWidth()-2,0) / mColumnWidth;
|
|
mRowsInWindow = clientHeight() / mRowHeight;
|
|
mContents.layout();
|
|
}
|
|
|
|
}
|
|
|
|
int QConsole::clientWidth()
|
|
{
|
|
return viewport()->size().width();
|
|
}
|
|
|
|
int QConsole::clientHeight()
|
|
{
|
|
return viewport()->size().height();
|
|
}
|
|
|
|
void QConsole::setTopRow(int value)
|
|
{
|
|
value = std::min(value,maxScrollHeight());
|
|
value = std::max(value, 1);
|
|
if (value != mTopRow) {
|
|
verticalScrollBar()->setValue(value);
|
|
}
|
|
}
|
|
|
|
int QConsole::maxScrollHeight()
|
|
{
|
|
return std::max(mContents.rows(),1);
|
|
}
|
|
|
|
void QConsole::updateScrollbars()
|
|
{
|
|
setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
|
|
setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
|
|
int nMaxScroll = maxScrollHeight();
|
|
int nMin = 1;
|
|
int nMax = std::max(1, nMaxScroll);
|
|
int nPage = mRowsInWindow;
|
|
int nPos = std::min(std::max(mTopRow,nMin),nMax);
|
|
verticalScrollBar()->setMinimum(nMin);
|
|
verticalScrollBar()->setMaximum(nMax);
|
|
verticalScrollBar()->setPageStep(nPage);
|
|
verticalScrollBar()->setSingleStep(1);
|
|
if (nPos != verticalScrollBar()->value())
|
|
verticalScrollBar()->setValue(nPos);
|
|
else
|
|
invalidate();
|
|
}
|
|
|
|
void QConsole::paintRows(QPainter &painter, int row1, int row2)
|
|
{
|
|
if (row1>row2)
|
|
return;
|
|
QRect rect(0,(row1-mTopRow)*mRowHeight,clientWidth(),(row2-row1+1)*mRowHeight);
|
|
painter.fillRect(rect,mBackground);
|
|
QStringList lst = mContents.getRows(row1,row2);
|
|
int startRow = row1-mTopRow;
|
|
painter.setPen(mForeground);
|
|
RowColumn selBeginRC = mContents.lineCharToRowColumn(selectionBegin());
|
|
RowColumn selEndRC = mContents.lineCharToRowColumn(selectionEnd());
|
|
LineChar editBegin = {
|
|
mContents.getLastLine().length() - mCurrentEditableLine.length(),
|
|
mContents.lines()-1
|
|
};
|
|
RowColumn editBeginRC = mContents.lineCharToRowColumn(editBegin);
|
|
bool isSelection = false;
|
|
painter.setPen(mForeground);
|
|
for (int i=0; i< lst.size(); i++) {
|
|
int currentRow = i+row1-1;
|
|
int left=2;
|
|
int top = (startRow+i) * mRowHeight;
|
|
int baseLine = (startRow+i+1)*mRowHeight - painter.fontMetrics().descent();
|
|
QString s = lst[i];
|
|
int columnsBefore = 0;
|
|
for (QChar ch:s) {
|
|
int charCol = charColumns(ch,columnsBefore);
|
|
int width = charCol * mColumnWidth;
|
|
if ((currentRow > selBeginRC.row ||
|
|
(currentRow == selBeginRC.row && columnsBefore>=selBeginRC.column))
|
|
&&
|
|
(currentRow < selEndRC.row ||
|
|
(currentRow == selEndRC.row && columnsBefore+charCol<=selEndRC.column))) {
|
|
if (!isSelection) {
|
|
isSelection = true;
|
|
}
|
|
if (!mReadonly &&(currentRow>editBeginRC.row ||
|
|
columnsBefore >= editBeginRC.column)) {
|
|
painter.setPen(mSelectionForeground);
|
|
painter.fillRect(left,top,width,mRowHeight,mSelectionBackground);
|
|
} else {
|
|
painter.setPen(mInactiveSelectionForeground);
|
|
painter.fillRect(left,top,width,mRowHeight,mInactiveSelectionBackground);
|
|
}
|
|
} else {
|
|
if (isSelection) {
|
|
isSelection = false;
|
|
painter.setPen(mForeground);
|
|
}
|
|
}
|
|
painter.drawText(left,baseLine,ch);
|
|
left+= width;
|
|
columnsBefore += charCol;
|
|
}
|
|
}
|
|
}
|
|
|
|
void QConsole::ensureCaretVisible()
|
|
{
|
|
int caretRow = mContents.rows();
|
|
if (caretRow < mTopRow) {
|
|
mTopRow = caretRow;
|
|
return;
|
|
}
|
|
if (caretRow >= mTopRow + mRowsInWindow) {
|
|
mTopRow = caretRow + 1 -mRowsInWindow;
|
|
}
|
|
}
|
|
|
|
void QConsole::showCaret()
|
|
{
|
|
if (mBlinkTimerId==0)
|
|
mBlinkTimerId = startTimer(500);
|
|
}
|
|
|
|
void QConsole::hideCaret()
|
|
{
|
|
if (mBlinkTimerId!=0) {
|
|
killTimer(mBlinkTimerId);
|
|
mBlinkTimerId = 0;
|
|
mBlinkStatus = 0;
|
|
updateCaret();
|
|
}
|
|
}
|
|
|
|
void QConsole::updateCaret()
|
|
{
|
|
QRect rcCaret = getCaretRect();
|
|
invalidateRect(rcCaret);
|
|
}
|
|
|
|
LineChar QConsole::caretPos()
|
|
{
|
|
QString lastLine = mContents.getLastLine();
|
|
int line = std::max(mContents.lines()-1,0);
|
|
int charIndex = 0;
|
|
if (mCaretChar>=mCurrentEditableLine.length()) {
|
|
charIndex = lastLine.length();
|
|
} else {
|
|
charIndex = lastLine.length()-mCurrentEditableLine.length()+mCaretChar;
|
|
}
|
|
return {charIndex,line};
|
|
}
|
|
|
|
RowColumn QConsole::caretRowColumn()
|
|
{
|
|
return mContents.lineCharToRowColumn(caretPos());
|
|
}
|
|
|
|
QPoint QConsole::rowColumnToPixels(const RowColumn &rowColumn)
|
|
{
|
|
/*
|
|
mTopRow is 1-based; rowColumn.row is 0-based
|
|
*/
|
|
int row =rowColumn.row+1 - mTopRow;
|
|
int col =rowColumn.column;
|
|
return QPoint(2+col*mColumnWidth, row*mRowHeight);
|
|
}
|
|
|
|
QRect QConsole::getCaretRect()
|
|
{
|
|
LineChar caret = caretPos();
|
|
QChar caretChar = mContents.getChar(caret);
|
|
RowColumn caretRC = mContents.lineCharToRowColumn(caret);
|
|
QPoint caretPos = rowColumnToPixels(caretRC);
|
|
int caretWidth=mColumnWidth;
|
|
//qDebug()<<"caret"<<mCaretX<<mCaretY;
|
|
int columnsBefore = caretRC.column;
|
|
if (!caretChar.isNull()) {
|
|
caretWidth = charColumns(caretChar, columnsBefore)*mColumnWidth;
|
|
}
|
|
return QRect(caretPos.x(),caretPos.y(),caretWidth,
|
|
mRowHeight);
|
|
}
|
|
|
|
void QConsole::doScrolled()
|
|
{
|
|
mTopRow = verticalScrollBar()->value();
|
|
invalidate();
|
|
}
|
|
|
|
void QConsole::contentsLayouted()
|
|
{
|
|
updateScrollbars();
|
|
}
|
|
|
|
void QConsole::contentsRowsAdded(int rowCount)
|
|
{
|
|
ensureCaretVisible();
|
|
updateScrollbars();
|
|
}
|
|
|
|
void QConsole::contentsLastRowsRemoved(int rowCount)
|
|
{
|
|
ensureCaretVisible();
|
|
updateScrollbars();
|
|
}
|
|
|
|
void QConsole::contentsLastRowsChanged(int rowCount)
|
|
{
|
|
ensureCaretVisible();
|
|
invalidateRows(mContents.rows()-rowCount+1,mContents.rows());
|
|
}
|
|
|
|
void QConsole::scrollTimerHandler()
|
|
|
|
{
|
|
QPoint iMousePos;
|
|
int X, Y;
|
|
|
|
iMousePos = QCursor::pos();
|
|
iMousePos = mapFromGlobal(iMousePos);
|
|
RowColumn mousePosRC = pixelsToNearestRowColumn(iMousePos.x(),iMousePos.y());
|
|
|
|
if (mScrollDeltaY != 0) {
|
|
qDebug()<<"scroll timer"<<mScrollDeltaY;
|
|
qDebug()<<mousePosRC.row<<mousePosRC.column;
|
|
if (QApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier))
|
|
setTopRow(mTopRow + mScrollDeltaY * mRowsInWindow);
|
|
else
|
|
setTopRow(mTopRow + mScrollDeltaY);
|
|
int row = mTopRow;
|
|
if (mScrollDeltaY > 0) // scrolling down?
|
|
row+=mRowsInWindow - 1;
|
|
mousePosRC.row = row - 1;
|
|
qDebug()<<row;
|
|
int oldStartRow = mContents.lineCharToRowColumn(selectionBegin()).row+1;
|
|
int oldEndRow = mContents.lineCharToRowColumn(selectionEnd()).row+1;
|
|
invalidateRows(oldStartRow,oldEndRow);
|
|
mSelectionEnd = mContents.rowColumnToLineChar(mousePosRC);
|
|
invalidateRows(row,row);
|
|
}
|
|
|
|
// computeScrollY(Y);
|
|
}
|
|
|
|
void QConsole::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
Qt::MouseButton button = event->button();
|
|
int X=event->pos().x();
|
|
int Y=event->pos().y();
|
|
|
|
QAbstractScrollArea::mousePressEvent(event);
|
|
|
|
//fKbdHandler.ExecuteMouseDown(Self, Button, Shift, X, Y);
|
|
|
|
if (button == Qt::LeftButton) {
|
|
setMouseTracking(true);
|
|
RowColumn mousePosRC = pixelsToNearestRowColumn(X,Y);
|
|
LineChar mousePos = mContents.rowColumnToLineChar(mousePosRC);
|
|
//I couldn't track down why, but sometimes (and definitely not all the time)
|
|
//the block positioning is lost. This makes sure that the block is
|
|
//maintained in case they started a drag operation on the block
|
|
int oldStartRow = mContents.lineCharToRowColumn(selectionBegin()).row+1;
|
|
int oldEndRow = mContents.lineCharToRowColumn(selectionEnd()).row+1;
|
|
invalidateRows(oldStartRow,oldEndRow);
|
|
mSelectionBegin = mousePos;
|
|
mSelectionEnd = mousePos;
|
|
}
|
|
}
|
|
|
|
void QConsole::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
QAbstractScrollArea::mouseReleaseEvent(event);
|
|
mScrollTimer->stop();
|
|
setMouseTracking(false);
|
|
|
|
}
|
|
|
|
void QConsole::mouseMoveEvent(QMouseEvent *event)
|
|
{
|
|
QAbstractScrollArea::mouseMoveEvent(event);
|
|
Qt::MouseButtons buttons = event->buttons();
|
|
int X=event->pos().x();
|
|
int Y=event->pos().y();
|
|
|
|
if ((buttons == Qt::LeftButton)) {
|
|
// should we begin scrolling?
|
|
computeScrollY(Y);
|
|
RowColumn mousePosRC = pixelsToNearestRowColumn(X, Y);
|
|
LineChar mousePos = mContents.rowColumnToLineChar(mousePosRC);
|
|
if (mScrollDeltaY == 0) {
|
|
int oldStartRow = mContents.lineCharToRowColumn(selectionBegin()).row+1;
|
|
int oldEndRow = mContents.lineCharToRowColumn(selectionEnd()).row+1;
|
|
invalidateRows(oldStartRow,oldEndRow);
|
|
mSelectionEnd = mousePos;
|
|
int row = mContents.lineCharToRowColumn(mSelectionEnd).row+1;
|
|
invalidateRows(row,row);
|
|
}
|
|
}
|
|
}
|
|
|
|
void QConsole::keyPressEvent(QKeyEvent *event)
|
|
{
|
|
switch(event->key()) {
|
|
case Qt::Key_Return:
|
|
event->accept();
|
|
if (mReadonly)
|
|
return;
|
|
emit commandInput(mCommand);
|
|
if (mHistorySize>0) {
|
|
if (mCommandHistory.size()==mHistorySize) {
|
|
mCommandHistory.pop_front();
|
|
mHistoryIndex--;
|
|
}
|
|
if (mCommandHistory.size()==0 || mHistoryIndex<0 || mHistoryIndex == mCommandHistory.size()-1) {
|
|
mHistoryIndex=mCommandHistory.size();
|
|
}
|
|
mCommandHistory.append(mCommand);
|
|
}
|
|
mCommand="";
|
|
addLine("");
|
|
return;
|
|
case Qt::Key_Up:
|
|
event->accept();
|
|
if (mHistorySize>0 && mHistoryIndex>0) {
|
|
mHistoryIndex--;
|
|
loadCommandFromHistory();
|
|
}
|
|
return;
|
|
case Qt::Key_Down:
|
|
event->accept();
|
|
if (mHistorySize>0 && mHistoryIndex<mCommandHistory.size()-1) {
|
|
mHistoryIndex++;
|
|
loadCommandFromHistory();
|
|
}
|
|
return;
|
|
case Qt::Key_Left:
|
|
event->accept();
|
|
if (mReadonly)
|
|
return;
|
|
if (mCaretChar>0 && mCaretChar<=mCurrentEditableLine.size()) {
|
|
setCaretChar(mCaretChar-1, !(event->modifiers() & Qt::ShiftModifier));
|
|
}
|
|
return;
|
|
case Qt::Key_Right:
|
|
event->accept();
|
|
if (mReadonly)
|
|
return;
|
|
if (mCaretChar<mCurrentEditableLine.size()) {
|
|
setCaretChar(mCaretChar+1, !(event->modifiers() & Qt::ShiftModifier));
|
|
}
|
|
return;
|
|
case Qt::Key_Home:
|
|
event->accept();
|
|
if (mReadonly)
|
|
return;
|
|
if (mCaretChar>0 && mCaretChar<=mCurrentEditableLine.size()) {
|
|
setCaretChar(0, !(event->modifiers() & Qt::ShiftModifier));
|
|
}
|
|
return;
|
|
case Qt::Key_End:
|
|
event->accept();
|
|
if (mReadonly)
|
|
return;
|
|
if (mCaretChar<mCurrentEditableLine.size()) {
|
|
setCaretChar(mCurrentEditableLine.size(), !(event->modifiers() & Qt::ShiftModifier));
|
|
}
|
|
return;
|
|
case Qt::Key_Backspace:
|
|
event->accept();
|
|
if (mReadonly)
|
|
return;
|
|
if (!mCurrentEditableLine.isEmpty() && (mCaretChar-1)<mCurrentEditableLine.size()
|
|
&&(mCaretChar-1>=0)) {
|
|
QString lastLine;
|
|
if (caretInSelection()) {
|
|
lastLine = removeSelection();
|
|
} else {
|
|
lastLine = mContents.getLastLine();
|
|
int len=mCurrentEditableLine.length();
|
|
mCaretChar--;
|
|
mCommand.remove(mCommand.length()-len+mCaretChar,1);
|
|
lastLine.remove(lastLine.length()-len+mCaretChar,1);
|
|
mCurrentEditableLine.remove(mCaretChar,1);
|
|
}
|
|
mContents.changeLastLine(lastLine);
|
|
mSelectionBegin = caretPos();
|
|
mSelectionEnd = caretPos();
|
|
} else {
|
|
if (hasSelection()) {
|
|
mSelectionBegin = caretPos();
|
|
mSelectionEnd = caretPos();
|
|
invalidate();
|
|
}
|
|
}
|
|
return;
|
|
case Qt::Key_Delete:
|
|
event->accept();
|
|
if (mReadonly)
|
|
return;
|
|
if (!mCurrentEditableLine.isEmpty() && (mCaretChar)<mCurrentEditableLine.size()
|
|
&&(mCaretChar>=0)) {
|
|
QString lastLine;
|
|
if (caretInSelection()) {
|
|
lastLine = removeSelection();
|
|
} else {
|
|
lastLine = mContents.getLastLine();
|
|
int len=mCurrentEditableLine.length();
|
|
mCommand.remove(mCommand.length()-len+mCaretChar,1);
|
|
lastLine.remove(lastLine.length()-len+mCaretChar,1);
|
|
mCurrentEditableLine.remove(mCaretChar,1);
|
|
}
|
|
mContents.changeLastLine(lastLine);
|
|
mSelectionBegin = caretPos();
|
|
mSelectionEnd = caretPos();
|
|
} else {
|
|
if (hasSelection()) {
|
|
mSelectionBegin = caretPos();
|
|
mSelectionEnd = caretPos();
|
|
invalidate();
|
|
}
|
|
}
|
|
return;
|
|
default:
|
|
if (!event->text().isEmpty()) {
|
|
event->accept();
|
|
if (mReadonly)
|
|
return;
|
|
textInputed(event->text());
|
|
return;
|
|
}
|
|
}
|
|
QAbstractScrollArea::keyPressEvent(event);
|
|
}
|
|
|
|
void QConsole::focusInEvent(QFocusEvent *)
|
|
{
|
|
showCaret();
|
|
}
|
|
|
|
void QConsole::focusOutEvent(QFocusEvent *)
|
|
{
|
|
hideCaret();
|
|
}
|
|
|
|
void QConsole::paintEvent(QPaintEvent *event)
|
|
{
|
|
if (mRowHeight==0)
|
|
return;
|
|
// Now paint everything while the caret is hidden.
|
|
QPainter painter(viewport());
|
|
//Get the invalidated rect.
|
|
QRect rcClip = event->rect();
|
|
QRect rcCaret= getCaretRect();
|
|
|
|
if (rcCaret == rcClip) {
|
|
// only update caret
|
|
painter.drawImage(rcCaret,*mContentImage,rcCaret);
|
|
} else {
|
|
int nL1, nL2;
|
|
// Compute the invalid area in lines
|
|
// lines
|
|
nL1 = std::min(std::max(mTopRow + rcClip.top() / mRowHeight, mTopRow), maxScrollHeight() + mRowsInWindow - 1 );
|
|
nL2 = std::min(std::max(mTopRow + (rcClip.bottom() + mRowHeight - 1) / mRowHeight, 1), maxScrollHeight() + mRowsInWindow - 1);
|
|
QPainter cachePainter(mContentImage.get());
|
|
cachePainter.setFont(font());
|
|
if (viewport()->rect() == rcClip) {
|
|
painter.fillRect(rcClip, mBackground);
|
|
}
|
|
paintRows(cachePainter,nL1,nL2);
|
|
painter.drawImage(rcClip,*mContentImage,rcClip);
|
|
}
|
|
paintCaret(painter, rcCaret);
|
|
}
|
|
|
|
void QConsole::paintCaret(QPainter &painter, const QRect rcClip)
|
|
{
|
|
if (mBlinkStatus!=1)
|
|
return;
|
|
painter.setClipRect(rcClip);
|
|
ConsoleCaretType ct = ConsoleCaretType::ctHorizontalLine;
|
|
QColor caretColor = mForeground;
|
|
switch(ct) {
|
|
case ConsoleCaretType::ctVerticalLine:
|
|
painter.fillRect(rcClip.left()+1,rcClip.top(),rcClip.left()+2,rcClip.bottom(),caretColor);
|
|
break;
|
|
case ConsoleCaretType::ctHorizontalLine:
|
|
painter.fillRect(rcClip.left(),rcClip.bottom()-2,rcClip.right(),rcClip.bottom()-1,caretColor);
|
|
break;
|
|
case ConsoleCaretType::ctBlock:
|
|
painter.fillRect(rcClip, caretColor);
|
|
break;
|
|
case ConsoleCaretType::ctHalfBlock:
|
|
QRect rc=rcClip;
|
|
rc.setTop(rcClip.top()+rcClip.height() / 2);
|
|
painter.fillRect(rcClip, caretColor);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void QConsole::textInputed(const QString &text)
|
|
{
|
|
if (mContents.rows()<=0) {
|
|
mContents.addLine("");
|
|
}
|
|
QString lastLine;
|
|
if (caretInSelection()) {
|
|
lastLine = removeSelection();
|
|
} else {
|
|
lastLine = mContents.getLastLine();
|
|
}
|
|
if (mCaretChar>=mCurrentEditableLine.size()) {
|
|
mCommand += text;
|
|
mCurrentEditableLine += text;
|
|
mCaretChar=mCurrentEditableLine.size();
|
|
mContents.changeLastLine(lastLine+text);
|
|
} else {
|
|
int len=mCurrentEditableLine.length();
|
|
mCommand.insert(mCommand.length()-len+mCaretChar,text);
|
|
lastLine.insert(lastLine.length()-len+mCaretChar,text);
|
|
mCurrentEditableLine.insert(mCaretChar,text);
|
|
mContents.changeLastLine(lastLine);
|
|
mCaretChar+=text.length();
|
|
}
|
|
mSelectionBegin = caretPos();
|
|
mSelectionEnd = caretPos();
|
|
}
|
|
|
|
void QConsole::loadCommandFromHistory()
|
|
{
|
|
if (mHistorySize<=0)
|
|
return;
|
|
mCommand = mCommandHistory[mHistoryIndex];
|
|
QString lastLine = mContents.getLastLine();
|
|
int len=mCurrentEditableLine.length();
|
|
lastLine.remove(lastLine.length()-len,INT_MAX);
|
|
mCurrentEditableLine=mCommand;
|
|
mCaretChar = mCurrentEditableLine.length();
|
|
mSelectionBegin = caretPos();
|
|
mSelectionEnd = caretPos();
|
|
mContents.changeLastLine(lastLine + mCommand);
|
|
}
|
|
|
|
LineChar QConsole::selectionBegin()
|
|
{
|
|
if (mSelectionBegin.line < mSelectionEnd.line ||
|
|
(mSelectionBegin.line == mSelectionEnd.line &&
|
|
mSelectionBegin.ch < mSelectionEnd.ch))
|
|
return mSelectionBegin;
|
|
return mSelectionEnd;
|
|
}
|
|
|
|
LineChar QConsole::selectionEnd()
|
|
{
|
|
if (mSelectionBegin.line < mSelectionEnd.line ||
|
|
(mSelectionBegin.line == mSelectionEnd.line &&
|
|
mSelectionBegin.ch < mSelectionEnd.ch))
|
|
return mSelectionEnd;
|
|
return mSelectionBegin;
|
|
}
|
|
|
|
void QConsole::setCaretChar(int newCaretChar, bool resetSelection)
|
|
{
|
|
RowColumn oldPosRC = caretRowColumn();
|
|
RowColumn oldSelBegin = mContents.lineCharToRowColumn(selectionBegin());
|
|
RowColumn oldSelEnd = mContents.lineCharToRowColumn(selectionEnd());
|
|
int oldStartRow = std::min(std::min(oldPosRC.row,oldSelBegin.row),oldSelEnd.row);
|
|
int oldEndRow = std::max(std::max(oldPosRC.row,oldSelBegin.row),oldSelEnd.row);
|
|
mCaretChar = newCaretChar;
|
|
LineChar newPos = caretPos();
|
|
RowColumn newPosRC = mContents.lineCharToRowColumn(newPos);
|
|
if (resetSelection)
|
|
mSelectionBegin = newPos;
|
|
mSelectionEnd = newPos;
|
|
|
|
int startRow = std::min(newPosRC.row, oldStartRow)+1;
|
|
int endRow = std::max(newPosRC.row, oldEndRow)+1;
|
|
invalidateRows(startRow, endRow);
|
|
}
|
|
|
|
bool QConsole::caretInSelection()
|
|
{
|
|
if (!hasSelection())
|
|
return false;
|
|
LineChar selBegin = selectionBegin();
|
|
LineChar selEnd = selectionEnd();
|
|
QString lastline = mContents.getLastLine();
|
|
int editBeginChar = lastline.length() - mCurrentEditableLine.length();
|
|
if (selEnd.line == mContents.lines()-1 && selEnd.ch > editBeginChar ) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QString QConsole::removeSelection()
|
|
{
|
|
|
|
QString lastLine = mContents.getLastLine();
|
|
int len=mCurrentEditableLine.length();
|
|
LineChar selBegin = selectionBegin();
|
|
LineChar selEnd = selectionEnd();
|
|
int selLen = selEnd.ch -selBegin.ch;
|
|
int ch = selBegin.ch -(lastLine.length()-len);
|
|
if (selBegin.line < mContents.lines()-1) {
|
|
mCaretChar = 0;
|
|
selLen = selEnd.ch - (lastLine.length()-len);
|
|
} else if (ch<0) {
|
|
mCaretChar = 0;
|
|
selLen = selLen + ch;
|
|
} else {
|
|
mCaretChar=ch;
|
|
}
|
|
mCommand.remove(mCommand.length()-len+mCaretChar,selLen);
|
|
lastLine.remove(lastLine.length()-len+mCaretChar,selLen);
|
|
mCurrentEditableLine.remove(mCaretChar,selLen);
|
|
return lastLine;
|
|
}
|
|
|
|
bool QConsole::hasSelection()
|
|
{
|
|
return (mSelectionBegin.line != mSelectionEnd.line)
|
|
|| (mSelectionBegin.ch != mSelectionEnd.ch);
|
|
}
|
|
|
|
int QConsole::computeScrollY(int Y)
|
|
{
|
|
QRect iScrollBounds = viewport()->rect();
|
|
if (Y < iScrollBounds.top())
|
|
mScrollDeltaY = (Y - iScrollBounds.top()) / mRowHeight - 1;
|
|
else if (Y >= iScrollBounds.bottom())
|
|
mScrollDeltaY = (Y - iScrollBounds.bottom()) / mRowHeight + 1;
|
|
else
|
|
mScrollDeltaY = 0;
|
|
|
|
if (mScrollDeltaY)
|
|
mScrollTimer->start();
|
|
return mScrollDeltaY;
|
|
}
|
|
|
|
RowColumn QConsole::pixelsToNearestRowColumn(int x, int y)
|
|
{
|
|
// Result is in display coordinates
|
|
// don't return a partially visible last line
|
|
if (y >= mRowsInWindow * mRowHeight) {
|
|
y = mRowsInWindow * mRowHeight - 1;
|
|
if (y < 0)
|
|
y = 0;
|
|
}
|
|
return {
|
|
std::max(0, (x - 2) / mColumnWidth),
|
|
mTopRow + (y / mRowHeight)-1
|
|
};
|
|
}
|
|
|
|
QString QConsole::lineBreak()
|
|
{
|
|
return "\r\n";
|
|
}
|
|
|
|
|
|
void QConsole::fontChanged()
|
|
{
|
|
recalcCharExtent();
|
|
sizeOrFontChanged(true);
|
|
}
|
|
|
|
bool QConsole::event(QEvent *event)
|
|
{
|
|
switch(event->type()) {
|
|
case QEvent::FontChange:
|
|
fontChanged();
|
|
break;
|
|
case QEvent::PaletteChange:
|
|
mBackground = palette().color(QPalette::Base);
|
|
mForeground = palette().color(QPalette::Text);
|
|
mSelectionBackground = palette().color(QPalette::Highlight);
|
|
mSelectionForeground = palette().color(QPalette::HighlightedText);
|
|
mInactiveSelectionBackground = palette().color(QPalette::Inactive,QPalette::Highlight);
|
|
mInactiveSelectionForeground = palette().color(QPalette::Inactive,QPalette::HighlightedText);
|
|
break;
|
|
}
|
|
return QAbstractScrollArea::event(event);
|
|
}
|
|
|
|
void QConsole::resizeEvent(QResizeEvent *)
|
|
{
|
|
//resize the cache image
|
|
std::shared_ptr<QImage> image = std::make_shared<QImage>(clientWidth(),clientHeight(),
|
|
QImage::Format_ARGB32);
|
|
if (mContentImage) {
|
|
QRect newRect = image->rect().intersected(mContentImage->rect());
|
|
|
|
QPainter painter(image.get());
|
|
painter.fillRect(viewport()->rect(),mBackground);
|
|
// painter.drawImage(newRect,*mContentImage);
|
|
}
|
|
|
|
mContentImage = image;
|
|
|
|
sizeOrFontChanged(false);
|
|
}
|
|
|
|
void QConsole::timerEvent(QTimerEvent *event)
|
|
{
|
|
if (event->timerId() == mBlinkTimerId) {
|
|
mBlinkStatus = 1- mBlinkStatus;
|
|
updateCaret();
|
|
}
|
|
}
|
|
|
|
void QConsole::inputMethodEvent(QInputMethodEvent *event)
|
|
{
|
|
if (mReadonly)
|
|
return;
|
|
QString s=event->commitString();
|
|
if (!s.isEmpty())
|
|
textInputed(s);
|
|
}
|
|
|
|
void QConsole::wheelEvent(QWheelEvent *event)
|
|
{
|
|
if (event->angleDelta().y()>0) {
|
|
verticalScrollBar()->setValue(verticalScrollBar()->value()-1);
|
|
event->accept();
|
|
return;
|
|
} else if (event->angleDelta().y()<0) {
|
|
verticalScrollBar()->setValue(verticalScrollBar()->value()+1);
|
|
event->accept();
|
|
return;
|
|
}
|
|
}
|
|
|
|
int ConsoleLines::rows() const
|
|
{
|
|
return mRows;
|
|
}
|
|
|
|
int ConsoleLines::lines() const
|
|
{
|
|
return mLines.count();
|
|
}
|
|
|
|
void ConsoleLines::layout()
|
|
{
|
|
if (!mConsole)
|
|
return;
|
|
if (mLayouting) {
|
|
mNeedRelayout = true;
|
|
return;
|
|
}
|
|
mLayouting = true;
|
|
mNeedRelayout = false;
|
|
emit layoutStarted();
|
|
mRows = 0;
|
|
bool forceUpdate = (mOldTabSize!=mConsole->tabSize());
|
|
for (PConsoleLine consoleLine: mLines) {
|
|
if (forceUpdate || consoleLine->maxColumns > mConsole->columnsPerRow()) {
|
|
consoleLine->maxColumns = breakLine(consoleLine->text,consoleLine->fragments);
|
|
}
|
|
mRows+=consoleLine->fragments.count();
|
|
}
|
|
emit layoutFinished();
|
|
mLayouting = false;
|
|
if (mNeedRelayout)
|
|
emit needRelayout();
|
|
}
|
|
|
|
ConsoleLines::ConsoleLines(QConsole *console)
|
|
{
|
|
mConsole = console;
|
|
mRows = 0;
|
|
mLayouting = false;
|
|
mNeedRelayout = false;
|
|
mOldTabSize = -1;
|
|
mMaxLines = 1000;
|
|
connect(this,&ConsoleLines::needRelayout,this,&ConsoleLines::layout);
|
|
}
|
|
|
|
void ConsoleLines::addLine(const QString &line)
|
|
{
|
|
PConsoleLine consoleLine=std::make_shared<ConsoleLine>();
|
|
consoleLine->text = line;
|
|
consoleLine->maxColumns = breakLine(line,consoleLine->fragments);
|
|
if (mLines.count()<mMaxLines || mMaxLines <= 0) {
|
|
mLines.append(consoleLine);
|
|
mRows += consoleLine->fragments.count();
|
|
emit rowsAdded(consoleLine->fragments.count());
|
|
} else {
|
|
PConsoleLine firstLine = mLines[0];
|
|
mLines.pop_front();
|
|
mRows -= firstLine->fragments.count();
|
|
mLines.append(consoleLine);
|
|
mRows += consoleLine->fragments.count();
|
|
emit layoutStarted();
|
|
emit layoutFinished();
|
|
}
|
|
}
|
|
|
|
void ConsoleLines::RemoveLastLine()
|
|
{
|
|
if (mLines.count()<=0)
|
|
return;
|
|
PConsoleLine consoleLine = mLines[mLines.count()-1];
|
|
mLines.pop_back();
|
|
mRows -= consoleLine->fragments.count();
|
|
emit lastRowsRemoved(consoleLine->fragments.count());
|
|
}
|
|
|
|
void ConsoleLines::changeLastLine(const QString &newLine)
|
|
{
|
|
if (mLines.count()<=0) {
|
|
return;
|
|
}
|
|
PConsoleLine consoleLine = mLines[mLines.count()-1];
|
|
int oldRows = consoleLine->fragments.count();
|
|
consoleLine->text = newLine;
|
|
breakLine(newLine,consoleLine->fragments);
|
|
int newRows = consoleLine->fragments.count();
|
|
if (newRows == oldRows) {
|
|
emit lastRowsChanged(oldRows);
|
|
return ;
|
|
} else {
|
|
mRows -= oldRows;
|
|
mRows += newRows;
|
|
emit layoutStarted();
|
|
emit layoutFinished();
|
|
}
|
|
}
|
|
|
|
QString ConsoleLines::getLastLine()
|
|
{
|
|
if (mLines.count()<=0)
|
|
return "";
|
|
return mLines[mLines.count()-1]->text;
|
|
}
|
|
|
|
QString ConsoleLines::getLine(int line)
|
|
{
|
|
if (line>=0 && line < mLines.count()) {
|
|
return mLines[line]->text;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
QChar ConsoleLines::getChar(int line, int ch)
|
|
{
|
|
QString s = getLine(line);
|
|
if (ch>=0 && ch<s.length()) {
|
|
return s[ch];
|
|
} else {
|
|
return QChar();
|
|
}
|
|
}
|
|
|
|
QChar ConsoleLines::getChar(const LineChar &lineChar)
|
|
{
|
|
return getChar(lineChar.line,lineChar.ch);
|
|
}
|
|
|
|
QStringList ConsoleLines::getRows(int startRow, int endRow)
|
|
{
|
|
if (startRow>mRows)
|
|
return QStringList();
|
|
if (startRow > endRow)
|
|
return QStringList();
|
|
QStringList lst;
|
|
int row = 0;
|
|
for (PConsoleLine line:mLines) {
|
|
for (const QString& s:line->fragments) {
|
|
row+=1;
|
|
if (row>endRow) {
|
|
return lst;
|
|
}
|
|
if (row>=startRow) {
|
|
lst.append(s);
|
|
}
|
|
}
|
|
}
|
|
return lst;
|
|
}
|
|
|
|
LineChar ConsoleLines::rowColumnToLineChar(const RowColumn &rowColumn)
|
|
{
|
|
return rowColumnToLineChar(rowColumn.row,rowColumn.column);
|
|
}
|
|
|
|
LineChar ConsoleLines::rowColumnToLineChar(int row, int column)
|
|
{
|
|
LineChar result{column,mLines.size()-1};
|
|
int rows=0;
|
|
for (int i=0;i<mLines.size();i++) {
|
|
PConsoleLine line = mLines[i];
|
|
if (row >= rows && row<rows+line->fragments.size()) {
|
|
int r=row - rows;
|
|
QString fragment = line->fragments[r];
|
|
int columnsBefore = 0;
|
|
for (int j=0;j<fragment.size();j++) {
|
|
QChar ch = fragment[j];
|
|
int charColumns= mConsole->charColumns(ch, columnsBefore);
|
|
if (column>=columnsBefore && column<columnsBefore+charColumns) {
|
|
result.ch = j;
|
|
break;
|
|
}
|
|
}
|
|
result.line = i;
|
|
break;
|
|
}
|
|
rows += line->fragments.size();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
RowColumn ConsoleLines::lineCharToRowColumn(const LineChar &lineChar)
|
|
{
|
|
return lineCharToRowColumn(lineChar.line,lineChar.ch);
|
|
}
|
|
|
|
RowColumn ConsoleLines::lineCharToRowColumn(int line, int ch)
|
|
{
|
|
RowColumn result{ch,std::max(0,mRows-1)};
|
|
int rowsBefore = 0;
|
|
if (line>=0 && line < mLines.size()) {
|
|
for (int i=0;i<line;i++) {
|
|
int rows = mLines[i]->fragments.size();
|
|
rowsBefore += rows;
|
|
}
|
|
PConsoleLine consoleLine = mLines[line];
|
|
int charsBefore = 0;
|
|
for (int r=0;r<consoleLine->fragments.size();r++) {
|
|
int chars = consoleLine->fragments[r].size();
|
|
if (r==consoleLine->fragments.size()-1 || (ch>=charsBefore && ch<charsBefore+chars)) {
|
|
QString fragment = consoleLine->fragments[r];
|
|
int columnsBefore = 0;
|
|
int len = std::min(ch-charsBefore,fragment.size());
|
|
for (int j=0;j<len;j++) {
|
|
QChar ch = fragment[j];
|
|
int charColumns = mConsole->charColumns(ch,columnsBefore);
|
|
columnsBefore += charColumns;
|
|
}
|
|
result.column=columnsBefore;
|
|
result.row = rowsBefore + r;
|
|
break;
|
|
}
|
|
charsBefore += chars;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool ConsoleLines::layouting() const
|
|
{
|
|
return mLayouting;
|
|
}
|
|
|
|
int ConsoleLines::breakLine(const QString &line, QStringList &fragments)
|
|
{
|
|
fragments.clear();
|
|
QString s = "";
|
|
int maxColLen = 0;
|
|
int columnsBefore = 0;
|
|
for (QChar ch:line) {
|
|
int charColumn = mConsole->charColumns(ch,columnsBefore);
|
|
if (charColumn + columnsBefore > mConsole->columnsPerRow()) {
|
|
if (ch == '\t') {
|
|
if (columnsBefore != mConsole->columnsPerRow()) {
|
|
charColumn = 0;
|
|
} else
|
|
charColumn = mConsole->tabSize();
|
|
}
|
|
fragments.append(s);
|
|
if (columnsBefore > maxColLen) {
|
|
maxColLen = columnsBefore;
|
|
}
|
|
s = "";
|
|
columnsBefore = 0;
|
|
}
|
|
if (charColumn > 0) {
|
|
columnsBefore += charColumn;
|
|
s += ch;
|
|
}
|
|
}
|
|
if (fragments.count() == 0 || !s.isEmpty()) {
|
|
fragments.append(s);
|
|
if (columnsBefore > maxColLen) {
|
|
maxColLen = columnsBefore;
|
|
}
|
|
}
|
|
return maxColLen;
|
|
}
|
|
|
|
int ConsoleLines::getMaxLines() const
|
|
{
|
|
return mMaxLines;
|
|
}
|
|
|
|
int ConsoleLines::maxLines() const
|
|
{
|
|
return mMaxLines;
|
|
}
|
|
|
|
void ConsoleLines::setMaxLines(int maxLines)
|
|
{
|
|
mMaxLines = maxLines;
|
|
if (mMaxLines > 0) {
|
|
while (mLines.count()>mMaxLines) {
|
|
mLines.pop_front();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ConsoleLines::clear()
|
|
{
|
|
mLines.clear();
|
|
mRows = 0;
|
|
}
|