File indexing completed on 2024-05-12 17:21:58

0001 /*
0002     SPDX-FileCopyrightText: 2009 Csaba Karai <krusader@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2009-2022 Krusader Krew <https://krusader.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "lister.h"
0009 
0010 // QtCore
0011 #include <QDate>
0012 #include <QFile>
0013 #include <QRect>
0014 #include <QTemporaryFile>
0015 #include <QTextStream>
0016 // QtGui
0017 #include <QClipboard>
0018 #include <QFontDatabase>
0019 #include <QFontMetrics>
0020 #include <QKeyEvent>
0021 #include <QPainter>
0022 // QtWidgets
0023 #include <QApplication>
0024 #include <QFileDialog>
0025 #include <QHBoxLayout>
0026 #include <QInputDialog>
0027 #include <QLabel>
0028 #include <QLineEdit>
0029 #include <QMenu>
0030 #include <QProgressBar>
0031 #include <QPushButton>
0032 #include <QScrollBar>
0033 #include <QSpacerItem>
0034 #include <QToolButton>
0035 // QtPrintSupport
0036 #include <QPrintDialog>
0037 #include <QPrinter>
0038 
0039 #include <KCodecs/KCharsets>
0040 #include <KConfigCore/KSharedConfig>
0041 #include <KCoreAddons/KJobTrackerInterface>
0042 #include <KI18n/KLocalizedString>
0043 #include <KIO/CopyJob>
0044 #include <KIO/JobUiDelegate>
0045 #include <KIO/TransferJob>
0046 #include <KParts/GUIActivateEvent>
0047 #include <KWidgetsAddons/KMessageBox>
0048 #include <KXmlGui/KActionCollection>
0049 
0050 #include "../GUI/krremoteencodingmenu.h"
0051 #include "../compat.h"
0052 #include "../icon.h"
0053 #include "../kractions.h"
0054 #include "../krglobal.h"
0055 
0056 #define SEARCH_CACHE_CHARS 100000
0057 #define SEARCH_MAX_ROW_LEN 4000
0058 #define CONTROL_CHAR 752
0059 #define CACHE_SIZE 1048576 // cache size set to 1MiB
0060 
0061 ListerTextArea::ListerTextArea(Lister *lister, QWidget *parent)
0062     : KTextEdit(parent)
0063     , _lister(lister)
0064 {
0065     connect(this, &QTextEdit::cursorPositionChanged, this, &ListerTextArea::slotCursorPositionChanged);
0066     _tabWidth = 4;
0067     setWordWrapMode(QTextOption::NoWrap);
0068     setLineWrapMode(QTextEdit::NoWrap);
0069 
0070     // zoom shortcuts
0071     connect(new QShortcut(QKeySequence("Ctrl++"), this), &QShortcut::activated, this, [=]() {
0072         zoomIn();
0073     });
0074     connect(new QShortcut(QKeySequence("Ctrl+-"), this), &QShortcut::activated, this, [=]() {
0075         zoomOut();
0076     });
0077 
0078     // start cursor blinking
0079     connect(&_blinkTimer, &QTimer::timeout, this, [=] {
0080         if (!_cursorBlinkMutex.tryLock()) {
0081             return;
0082         }
0083         setCursorWidth(cursorWidth() == 0 ? 2 : 0);
0084         _cursorBlinkMutex.unlock();
0085     });
0086     _blinkTimer.start(500);
0087 }
0088 
0089 void ListerTextArea::reset()
0090 {
0091     _screenStartPos = 0;
0092     _cursorPos = 0;
0093     _cursorAnchorPos = -1;
0094     _cursorAtFirstColumn = true;
0095     calculateText();
0096 }
0097 
0098 void ListerTextArea::sizeChanged()
0099 {
0100     if (_cursorAnchorPos > _lister->fileSize())
0101         _cursorAnchorPos = -1;
0102     if (_cursorPos > _lister->fileSize())
0103         _cursorPos = _lister->fileSize();
0104 
0105     redrawTextArea(true);
0106 }
0107 
0108 void ListerTextArea::resizeEvent(QResizeEvent *event)
0109 {
0110     KTextEdit::resizeEvent(event);
0111     redrawTextArea();
0112 }
0113 
0114 void ListerTextArea::calculateText(const bool forcedUpdate)
0115 {
0116     const QRect contentRect = viewport()->contentsRect();
0117     const QFontMetrics fm(font());
0118 
0119     const int fontHeight = std::max(fm.height(), 1);
0120 
0121     // This is quite accurate (although not perfect) way of getting
0122     // a single character width along with its surrounding space.
0123     const double fontWidth = (fm.horizontalAdvance("WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW")
0124                               - fm.horizontalAdvance("W"))
0125         / 99.0;
0126 
0127     const int sizeY = contentRect.height() / fontHeight;
0128     _pageSize = sizeY;
0129 
0130     const int textViewportWidth = std::max(contentRect.width() - (int)fontWidth, 0);
0131 
0132     setTabStopDistance(fontWidth * _tabWidth);
0133 
0134     const int sizeX = static_cast<int>(textViewportWidth / fontWidth);
0135 
0136     _sizeChanged = (_sizeY != sizeY) || (_sizeX != sizeX) || forcedUpdate;
0137     _sizeY = sizeY;
0138     _sizeX = sizeX;
0139 
0140     QList<qint64> rowStarts;
0141 
0142     QStringList list = readLines(_screenStartPos, _screenEndPos, _sizeY, &rowStarts);
0143 
0144     if (_sizeChanged) {
0145         _averagePageSize = _screenEndPos - _screenStartPos;
0146         setUpScrollBar();
0147     }
0148 
0149     const QStringList listRemn = readLines(_screenEndPos, _screenEndPos, 1);
0150     list << listRemn;
0151 
0152     if (list != _rowContent) {
0153         _cursorBlinkMutex.lock();
0154         _blinkTimer.stop();
0155         setCursorWidth(0);
0156 
0157         setPlainText(list.join("\n"));
0158 
0159         if (_cursorAnchorPos == -1 || _cursorAnchorPos == _cursorPos) {
0160             clearSelection();
0161             _blinkTimer.start(500);
0162         }
0163 
0164         _cursorBlinkMutex.unlock();
0165 
0166         _rowContent = list;
0167         _rowStarts = rowStarts;
0168         if (_rowStarts.size() < _sizeY) {
0169             _rowStarts << _screenEndPos;
0170         }
0171     }
0172 }
0173 
0174 qint64 ListerTextArea::textToFilePositionOnScreen(const int x, const int y, bool &isfirst)
0175 {
0176     isfirst = (x == 0);
0177     if (y >= _rowStarts.count()) {
0178         return 0;
0179     }
0180     const qint64 rowStart = _rowStarts[y];
0181     if (x == 0) {
0182         return rowStart;
0183     }
0184 
0185     if (_hexMode) {
0186         const qint64 pos = rowStart + _lister->hexPositionToIndex(_sizeX, x);
0187         if (pos > _lister->fileSize()) {
0188             return _lister->fileSize();
0189         }
0190         return pos;
0191     }
0192 
0193     // we can't use fromUnicode because of the invalid encoded chars
0194     const int maxBytes = 2 * _sizeX * MAX_CHAR_LENGTH;
0195     QByteArray chunk = _lister->cacheChunk(rowStart, maxBytes);
0196 
0197     QTextStream stream(&chunk);
0198     stream.setCodec(_lister->codec());
0199     stream.read(x);
0200     return rowStart + stream.pos();
0201 }
0202 
0203 void ListerTextArea::fileToTextPositionOnScreen(const qint64 p, const bool isfirst, int &x, int &y)
0204 {
0205     // check if cursor is outside of visible area
0206     if (p < _screenStartPos || p > _screenEndPos || _rowStarts.count() < 1) {
0207         x = -1;
0208         y = (p > _screenEndPos) ? -2 : -1;
0209         return;
0210     }
0211 
0212     // find row
0213     y = 0;
0214     while (y < _rowStarts.count() && _rowStarts[y] <= p) {
0215         y++;
0216     }
0217     y--;
0218     if (y < 0) {
0219         x = y = -1;
0220         return;
0221     }
0222 
0223     const qint64 rowStart = _rowStarts[y];
0224     if (_hexMode) {
0225         x = _lister->hexIndexToPosition(_sizeX, (int)(p - rowStart));
0226         return;
0227     }
0228 
0229     // find column
0230     const int maxBytes = 2 * _sizeX * MAX_CHAR_LENGTH;
0231     x = 0;
0232     if (rowStart >= p) {
0233         if ((rowStart == p) && !isfirst && y > 0) {
0234             const qint64 previousRow = _rowStarts[y - 1];
0235             const QByteArray chunk = _lister->cacheChunk(previousRow, maxBytes);
0236             QByteArray cachedBuffer = chunk.left(static_cast<int>(p - previousRow));
0237 
0238             QTextStream stream(&cachedBuffer);
0239             stream.setCodec(_lister->codec());
0240             stream.read(_rowContent[y - 1].length());
0241             if (previousRow + stream.pos() == p) {
0242                 y--;
0243                 x = _rowContent[y].length();
0244             }
0245         }
0246         return;
0247     }
0248 
0249     const QByteArray chunk = _lister->cacheChunk(rowStart, maxBytes);
0250     const QByteArray cachedBuffer = chunk.left(static_cast<int>(p - rowStart));
0251 
0252     x = _lister->codec()->toUnicode(cachedBuffer).length();
0253 }
0254 
0255 void ListerTextArea::getCursorPosition(int &x, int &y)
0256 {
0257     getScreenPosition(textCursor().position(), x, y);
0258 }
0259 
0260 void ListerTextArea::getScreenPosition(const int position, int &x, int &y)
0261 {
0262     x = position;
0263     y = 0;
0264     foreach (const QString &row, _rowContent) {
0265         const int rowLen = row.length() + 1;
0266         if (x < rowLen) {
0267             return;
0268         }
0269         x -= rowLen;
0270         y++;
0271     }
0272 }
0273 
0274 void ListerTextArea::setCursorPositionOnScreen(const int x, const int y, const int anchorX, const int anchorY)
0275 {
0276     setCursorWidth(0);
0277 
0278     int finalX = x;
0279     int finalY = y;
0280 
0281     if (finalX == -1 || finalY < 0) {
0282         if (anchorY == -1) {
0283             return;
0284         }
0285 
0286         if (finalY == -2) {
0287             finalY = _sizeY;
0288             finalX = (_rowContent.count() > _sizeY) ? _rowContent[_sizeY].length() : 0;
0289         } else
0290             finalX = finalY = 0;
0291     }
0292 
0293     const int realSizeY = std::min(_sizeY + 1, _rowContent.count());
0294 
0295     const auto setUpCursor = [&](const int cursorX, const int cursorY, const QTextCursor::MoveMode mode) -> bool {
0296         if (cursorY > realSizeY) {
0297             return false;
0298         }
0299 
0300         _skipCursorChangedListener = true;
0301 
0302         moveCursor(QTextCursor::Start, mode);
0303         for (int i = 0; i < cursorY; i++) {
0304             moveCursor(QTextCursor::Down, mode);
0305         }
0306 
0307         int finalCursorX = cursorX;
0308         if (_rowContent.count() > cursorY && finalCursorX > _rowContent[cursorY].length()) {
0309             finalCursorX = _rowContent[cursorY].length();
0310         }
0311 
0312         for (int i = 0; i < finalCursorX; i++) {
0313             moveCursor(QTextCursor::Right, mode);
0314         }
0315 
0316         _skipCursorChangedListener = false;
0317 
0318         return true;
0319     };
0320 
0321     QTextCursor::MoveMode mode = QTextCursor::MoveAnchor;
0322 
0323     // set cursor anchor
0324     if (anchorX != -1 && anchorY != -1) {
0325         const bool canContinue = setUpCursor(anchorX, anchorY, mode);
0326         if (!canContinue) {
0327             return;
0328         }
0329 
0330         mode = QTextCursor::KeepAnchor;
0331     }
0332 
0333     // set cursor position
0334     setUpCursor(finalX, finalY, mode);
0335 }
0336 
0337 qint64 ListerTextArea::getCursorPosition(bool &isfirst)
0338 {
0339     if (cursorWidth() == 0) {
0340         isfirst = _cursorAtFirstColumn;
0341         return _cursorPos;
0342     }
0343 
0344     int x, y;
0345     getCursorPosition(x, y);
0346     return textToFilePositionOnScreen(x, y, isfirst);
0347 }
0348 
0349 void ListerTextArea::setCursorPositionInDocument(const qint64 p, const bool isfirst)
0350 {
0351     _cursorPos = p;
0352     int x, y;
0353     fileToTextPositionOnScreen(p, isfirst, x, y);
0354 
0355     bool startBlinkTimer = _screenStartPos <= _cursorPos && _cursorPos <= _screenEndPos;
0356     int anchorX = -1, anchorY = -1;
0357     if (_cursorAnchorPos != -1 && _cursorAnchorPos != p) {
0358         qint64 anchPos = _cursorAnchorPos;
0359         bool anchorBelow = false, anchorAbove = false;
0360         if (anchPos < _screenStartPos) {
0361             anchPos = _screenStartPos;
0362             anchorY = -2;
0363             anchorAbove = true;
0364         }
0365         if (anchPos > _screenEndPos) {
0366             anchPos = _screenEndPos;
0367             anchorY = -3;
0368             anchorBelow = true;
0369         }
0370 
0371         fileToTextPositionOnScreen(anchPos, isfirst, anchorX, anchorY);
0372 
0373         if (_hexMode) {
0374             if (anchorAbove) {
0375                 anchorX = 0;
0376             }
0377             if (anchorBelow && _rowContent.count() > 0) {
0378                 anchorX = _rowContent[0].length();
0379             }
0380         }
0381 
0382         startBlinkTimer = startBlinkTimer && !anchorAbove && !anchorBelow;
0383     }
0384     if (startBlinkTimer) {
0385         _blinkTimer.start(500);
0386     }
0387     setCursorPositionOnScreen(x, y, anchorX, anchorY);
0388     _lister->slotUpdate();
0389 }
0390 
0391 void ListerTextArea::slotCursorPositionChanged()
0392 {
0393     if (_skipCursorChangedListener) {
0394         return;
0395     }
0396     int cursorX, cursorY;
0397     getCursorPosition(cursorX, cursorY);
0398     _cursorAtFirstColumn = (cursorX == 0);
0399     _cursorPos = textToFilePositionOnScreen(cursorX, cursorY, _cursorAtFirstColumn);
0400     _lister->slotUpdate();
0401 }
0402 
0403 QString ListerTextArea::readSection(const qint64 p1, const qint64 p2)
0404 {
0405     if (p1 == p2)
0406         return QString();
0407 
0408     qint64 sel1 = p1;
0409     qint64 sel2 = p2;
0410     if (sel1 > sel2) {
0411         std::swap(sel1, sel2);
0412     }
0413 
0414     QString section;
0415 
0416     if (_hexMode) {
0417         while (sel1 != sel2) {
0418             const QStringList list = _lister->readHexLines(sel1, sel2, _sizeX, 1);
0419             if (list.isEmpty()) {
0420                 break;
0421             }
0422             if (!section.isEmpty()) {
0423                 section += QChar('\n');
0424             }
0425             section += list.at(0);
0426         }
0427         return section;
0428     }
0429 
0430     qint64 pos = sel1;
0431 
0432     QScopedPointer<QTextDecoder> decoder(_lister->codec()->makeDecoder());
0433 
0434     do {
0435         const int maxBytes = std::min(_sizeX * _sizeY * MAX_CHAR_LENGTH, (int)(sel2 - pos));
0436         const QByteArray chunk = _lister->cacheChunk(pos, maxBytes);
0437         if (chunk.isEmpty())
0438             break;
0439         section += decoder->toUnicode(chunk);
0440         pos += chunk.size();
0441     } while (pos < sel2);
0442 
0443     return section;
0444 }
0445 
0446 QStringList ListerTextArea::readLines(qint64 filePos, qint64 &endPos, const int lines, QList<qint64> *locs)
0447 {
0448     QStringList list;
0449 
0450     if (_hexMode) {
0451         endPos = _lister->fileSize();
0452         if (filePos >= endPos) {
0453             return list;
0454         }
0455         const int bytes = _lister->hexBytesPerLine(_sizeX);
0456         qint64 startPos = (filePos / bytes) * bytes;
0457         qint64 shiftPos = startPos;
0458         list = _lister->readHexLines(shiftPos, endPos, _sizeX, lines);
0459         endPos = shiftPos;
0460         if (locs) {
0461             for (int i = 0; i < list.count(); i++) {
0462                 (*locs) << startPos;
0463                 startPos += bytes;
0464             }
0465         }
0466         return list;
0467     }
0468 
0469     endPos = filePos;
0470     const int maxBytes = _sizeX * _sizeY * MAX_CHAR_LENGTH;
0471     const QByteArray chunk = _lister->cacheChunk(filePos, maxBytes);
0472     if (chunk.isEmpty())
0473         return list;
0474 
0475     int byteCounter = 0;
0476     QString row = "";
0477     int effLength = 0;
0478     if (locs)
0479         (*locs) << filePos;
0480     bool skipImmediateNewline = false;
0481 
0482     const auto performNewline = [&](qint64 nextRowStartOffset) {
0483         list << row;
0484         effLength = 0;
0485         row = "";
0486         if (locs) {
0487             (*locs) << (filePos + nextRowStartOffset);
0488         }
0489     };
0490 
0491     QScopedPointer<QTextDecoder> decoder(_lister->codec()->makeDecoder());
0492 
0493     while (byteCounter < chunk.size() && list.size() < lines) {
0494         const int lastCnt = byteCounter;
0495         QString chr = decoder->toUnicode(chunk.mid(byteCounter++, 1));
0496         if (chr.isEmpty()) {
0497             continue;
0498         }
0499 
0500         if ((chr[0] < 32) && (chr[0] != '\n') && (chr[0] != '\t')) {
0501             chr = QChar(CONTROL_CHAR);
0502         }
0503 
0504         if (chr == "\n") {
0505             if (!skipImmediateNewline) {
0506                 performNewline(byteCounter);
0507             }
0508             skipImmediateNewline = false;
0509             continue;
0510         }
0511 
0512         skipImmediateNewline = false;
0513 
0514         if (chr == "\t") {
0515             effLength += _tabWidth - (effLength % _tabWidth) - 1;
0516             if (effLength > _sizeX) {
0517                 performNewline(lastCnt);
0518             }
0519         }
0520         row += chr;
0521         effLength++;
0522         if (effLength >= _sizeX) {
0523             performNewline(byteCounter);
0524             skipImmediateNewline = true;
0525         }
0526     }
0527 
0528     if (list.size() < lines)
0529         list << row;
0530 
0531     endPos = filePos + byteCounter;
0532 
0533     return list;
0534 }
0535 
0536 void ListerTextArea::setUpScrollBar()
0537 {
0538     if (_averagePageSize == _lister->fileSize()) {
0539         _lister->scrollBar()->setPageStep(0);
0540         _lister->scrollBar()->setMaximum(0);
0541         _lister->scrollBar()->hide();
0542         _lastPageStartPos = 0;
0543     } else {
0544         const int maxPage = MAX_CHAR_LENGTH * _sizeX * _sizeY;
0545         qint64 pageStartPos = _lister->fileSize() - maxPage;
0546         qint64 endPos;
0547         if (pageStartPos < 0)
0548             pageStartPos = 0;
0549         QStringList list = readLines(pageStartPos, endPos, maxPage);
0550         if (list.count() <= _sizeY) {
0551             _lastPageStartPos = 0;
0552         } else {
0553             readLines(pageStartPos, _lastPageStartPos, list.count() - _sizeY);
0554         }
0555 
0556         const qint64 maximum = (_lastPageStartPos > SLIDER_MAX) ? SLIDER_MAX : _lastPageStartPos;
0557         qint64 pageSize = (_lastPageStartPos > SLIDER_MAX) ? SLIDER_MAX * _averagePageSize / _lastPageStartPos : _averagePageSize;
0558         if (pageSize == 0)
0559             pageSize++;
0560 
0561         _lister->scrollBar()->setPageStep(static_cast<int>(pageSize));
0562         _lister->scrollBar()->setMaximum(static_cast<int>(maximum));
0563         _lister->scrollBar()->show();
0564     }
0565 }
0566 
0567 void ListerTextArea::keyPressEvent(QKeyEvent *ke)
0568 {
0569     if (KrGlobal::copyShortcut == QKeySequence(ke->key() | ke->modifiers())) {
0570         copySelectedToClipboard();
0571         ke->accept();
0572         return;
0573     }
0574 
0575     if (ke->modifiers() == Qt::NoModifier || ke->modifiers() & Qt::ShiftModifier) {
0576         qint64 newAnchor = -1;
0577         if (ke->modifiers() & Qt::ShiftModifier) {
0578             newAnchor = _cursorAnchorPos;
0579             if (_cursorAnchorPos == -1)
0580                 newAnchor = _cursorPos;
0581         }
0582 
0583         switch (ke->key()) {
0584         case Qt::Key_F3:
0585             ke->accept();
0586             if (ke->modifiers() == Qt::ShiftModifier)
0587                 _lister->searchPrev();
0588             else
0589                 _lister->searchNext();
0590             return;
0591         case Qt::Key_Home:
0592         case Qt::Key_End:
0593             _cursorAnchorPos = newAnchor;
0594             break;
0595         case Qt::Key_Left: {
0596             _cursorAnchorPos = newAnchor;
0597             ensureVisibleCursor();
0598             int x, y;
0599             getCursorPosition(x, y);
0600             if (y == 0 && x == 0)
0601                 slotActionTriggered(QAbstractSlider::SliderSingleStepSub);
0602         } break;
0603         case Qt::Key_Right: {
0604             _cursorAnchorPos = newAnchor;
0605             ensureVisibleCursor();
0606             if (textCursor().position() == toPlainText().length())
0607                 slotActionTriggered(QAbstractSlider::SliderSingleStepAdd);
0608         } break;
0609         case Qt::Key_Up: {
0610             _cursorAnchorPos = newAnchor;
0611             ensureVisibleCursor();
0612             int x, y;
0613             getCursorPosition(x, y);
0614             if (y == 0)
0615                 slotActionTriggered(QAbstractSlider::SliderSingleStepSub);
0616         } break;
0617         case Qt::Key_Down: {
0618             _cursorAnchorPos = newAnchor;
0619             ensureVisibleCursor();
0620             int x, y;
0621             getCursorPosition(x, y);
0622             if (y >= _sizeY - 1)
0623                 slotActionTriggered(QAbstractSlider::SliderSingleStepAdd);
0624         } break;
0625         case Qt::Key_PageDown: {
0626             _cursorAnchorPos = newAnchor;
0627             ensureVisibleCursor();
0628             ke->accept();
0629             int x, y;
0630             getCursorPosition(x, y);
0631             slotActionTriggered(QAbstractSlider::SliderPageStepAdd);
0632             y += _sizeY - _skippedLines;
0633             if (y > _rowContent.count()) {
0634                 y = _rowContent.count() - 1;
0635                 if (y > 0)
0636                     x = _rowContent[y - 1].length();
0637                 else
0638                     x = 0;
0639             }
0640             _cursorPos = textToFilePositionOnScreen(x, y, _cursorAtFirstColumn);
0641             setCursorPositionInDocument(_cursorPos, _cursorAtFirstColumn);
0642         }
0643             return;
0644         case Qt::Key_PageUp: {
0645             _cursorAnchorPos = newAnchor;
0646             ensureVisibleCursor();
0647             ke->accept();
0648             int x, y;
0649             getCursorPosition(x, y);
0650             slotActionTriggered(QAbstractSlider::SliderPageStepSub);
0651             y -= _sizeY - _skippedLines;
0652             if (y < 0) {
0653                 y = 0;
0654                 x = 0;
0655             }
0656             _cursorPos = textToFilePositionOnScreen(x, y, _cursorAtFirstColumn);
0657             setCursorPositionInDocument(_cursorPos, _cursorAtFirstColumn);
0658         }
0659             return;
0660         }
0661     }
0662     if (ke->modifiers() == Qt::ControlModifier) {
0663         switch (ke->key()) {
0664         case Qt::Key_G:
0665             ke->accept();
0666             _lister->jumpToPosition();
0667             return;
0668         case Qt::Key_F:
0669             ke->accept();
0670             _lister->enableSearch(true);
0671             return;
0672         case Qt::Key_Home:
0673             _cursorAnchorPos = -1;
0674             ke->accept();
0675             slotActionTriggered(QAbstractSlider::SliderToMinimum);
0676             setCursorPositionInDocument((qint64)0, true);
0677             return;
0678         case Qt::Key_A:
0679         case Qt::Key_End: {
0680             _cursorAnchorPos = (ke->key() == Qt::Key_A) ? 0 : -1;
0681             ke->accept();
0682             slotActionTriggered(QAbstractSlider::SliderToMaximum);
0683             const qint64 endPos = _lister->fileSize();
0684             setCursorPositionInDocument(endPos, false);
0685             return;
0686         }
0687         case Qt::Key_Down:
0688             ke->accept();
0689             slotActionTriggered(QAbstractSlider::SliderSingleStepAdd);
0690             return;
0691         case Qt::Key_Up:
0692             ke->accept();
0693             slotActionTriggered(QAbstractSlider::SliderSingleStepSub);
0694             return;
0695         case Qt::Key_PageDown:
0696             ke->accept();
0697             slotActionTriggered(QAbstractSlider::SliderPageStepAdd);
0698             return;
0699         case Qt::Key_PageUp:
0700             ke->accept();
0701             slotActionTriggered(QAbstractSlider::SliderPageStepSub);
0702             return;
0703         }
0704     }
0705     const int oldAnchor = textCursor().anchor();
0706     KTextEdit::keyPressEvent(ke);
0707     handleAnchorChange(oldAnchor);
0708 }
0709 
0710 void ListerTextArea::mousePressEvent(QMouseEvent *e)
0711 {
0712     KTextEdit::mousePressEvent(e);
0713     // do change anchor only when shift is not pressed
0714     if (!(QGuiApplication::keyboardModifiers() & Qt::ShiftModifier)) {
0715         performAnchorChange(textCursor().anchor());
0716     }
0717 }
0718 
0719 void ListerTextArea::mouseDoubleClickEvent(QMouseEvent *e)
0720 {
0721     _cursorAnchorPos = -1;
0722     const int oldAnchor = textCursor().anchor();
0723     KTextEdit::mouseDoubleClickEvent(e);
0724     handleAnchorChange(oldAnchor);
0725 }
0726 
0727 void ListerTextArea::mouseMoveEvent(QMouseEvent *e)
0728 {
0729     if (e->pos().y() < 0) {
0730         slotActionTriggered(QAbstractSlider::SliderSingleStepSub);
0731     } else if (e->pos().y() > height()) {
0732         slotActionTriggered(QAbstractSlider::SliderSingleStepAdd);
0733     }
0734     KTextEdit::mouseMoveEvent(e);
0735 }
0736 
0737 void ListerTextArea::wheelEvent(QWheelEvent *e)
0738 {
0739     int delta = e->angleDelta().y();
0740     if (delta) {
0741         // zooming
0742         if (e->modifiers() & Qt::ControlModifier) {
0743             e->accept();
0744             if (delta > 0) {
0745                 zoomIn();
0746             } else {
0747                 zoomOut();
0748             }
0749             return;
0750         }
0751 
0752         if (delta > 0) {
0753             e->accept();
0754             while (delta > 0) {
0755                 slotActionTriggered(QAbstractSlider::SliderSingleStepSub);
0756                 slotActionTriggered(QAbstractSlider::SliderSingleStepSub);
0757                 slotActionTriggered(QAbstractSlider::SliderSingleStepSub);
0758                 delta -= 120;
0759             }
0760         } else {
0761             e->accept();
0762             while (delta < 0) {
0763                 slotActionTriggered(QAbstractSlider::SliderSingleStepAdd);
0764                 slotActionTriggered(QAbstractSlider::SliderSingleStepAdd);
0765                 slotActionTriggered(QAbstractSlider::SliderSingleStepAdd);
0766                 delta += 120;
0767             }
0768         }
0769 
0770         setCursorPositionInDocument(_cursorPos, false);
0771     }
0772 }
0773 
0774 void ListerTextArea::slotActionTriggered(int action)
0775 {
0776     switch (action) {
0777     case QAbstractSlider::SliderSingleStepAdd: {
0778         qint64 endPos;
0779         readLines(_screenStartPos, endPos, 1);
0780         if (endPos <= _lastPageStartPos) {
0781             _screenStartPos = endPos;
0782         }
0783     } break;
0784     case QAbstractSlider::SliderSingleStepSub: {
0785         if (_screenStartPos == 0) {
0786             break;
0787         }
0788 
0789         if (_hexMode) {
0790             int bytesPerRow = _lister->hexBytesPerLine(_sizeX);
0791             _screenStartPos = (_screenStartPos / bytesPerRow) * bytesPerRow;
0792             _screenStartPos -= bytesPerRow;
0793             if (_screenStartPos < 0) {
0794                 _screenStartPos = 0;
0795             }
0796             break;
0797         }
0798 
0799         qint64 maxSize = _sizeX * _sizeY * MAX_CHAR_LENGTH;
0800         const QByteArray encodedEnter = _lister->codec()->fromUnicode(QString("\n"));
0801 
0802         qint64 readPos = _screenStartPos - maxSize;
0803         if (readPos < 0) {
0804             readPos = 0;
0805         }
0806         maxSize = _screenStartPos - readPos;
0807 
0808         const QByteArray chunk = _lister->cacheChunk(readPos, maxSize);
0809 
0810         int from = chunk.size();
0811         while (from > 0) {
0812             from--;
0813             from = chunk.lastIndexOf(encodedEnter, from);
0814             if (from == -1) {
0815                 from = 0;
0816                 break;
0817             }
0818             const int backRef = std::max(from - 20, 0);
0819             const int size = from - backRef + encodedEnter.size();
0820             const QString decoded = _lister->codec()->toUnicode(chunk.mid(backRef, size));
0821             if (decoded.endsWith(QLatin1String("\n"))) {
0822                 if (from < (chunk.size() - encodedEnter.size())) {
0823                     from += encodedEnter.size();
0824                     break;
0825                 }
0826             }
0827         }
0828 
0829         readPos += from;
0830         qint64 previousPos = readPos;
0831         while (readPos < _screenStartPos) {
0832             previousPos = readPos;
0833             readLines(readPos, readPos, 1);
0834         }
0835         _screenStartPos = previousPos;
0836     } break;
0837     case QAbstractSlider::SliderPageStepAdd: {
0838         _skippedLines = 0;
0839 
0840         qint64 endPos;
0841         for (int i = 0; i < _sizeY; i++) {
0842             readLines(_screenStartPos, endPos, 1);
0843             if (endPos <= _lastPageStartPos) {
0844                 _screenStartPos = endPos;
0845                 _skippedLines++;
0846             } else {
0847                 break;
0848             }
0849         }
0850     } break;
0851     case QAbstractSlider::SliderPageStepSub: {
0852         _skippedLines = 0;
0853 
0854         if (_screenStartPos == 0) {
0855             break;
0856         }
0857 
0858         if (_hexMode) {
0859             const int bytesPerRow = _lister->hexBytesPerLine(_sizeX);
0860             _screenStartPos = (_screenStartPos / bytesPerRow) * bytesPerRow;
0861             _screenStartPos -= _sizeY * bytesPerRow;
0862             if (_screenStartPos < 0) {
0863                 _screenStartPos = 0;
0864             }
0865             break;
0866         }
0867 
0868         // text lister mode
0869         qint64 maxSize = 2 * _sizeX * _sizeY * MAX_CHAR_LENGTH;
0870         const QByteArray encodedEnter = _lister->codec()->fromUnicode(QString("\n"));
0871 
0872         qint64 readPos = _screenStartPos - maxSize;
0873         if (readPos < 0)
0874             readPos = 0;
0875         maxSize = _screenStartPos - readPos;
0876 
0877         const QByteArray chunk = _lister->cacheChunk(readPos, maxSize);
0878         maxSize = chunk.size();
0879 
0880         int sizeY = _sizeY + 1;
0881         int origSizeY = sizeY;
0882         qint64 from = maxSize;
0883         qint64 lastEnter = maxSize;
0884 
0885         bool readNext = true;
0886         while (readNext) {
0887             readNext = false;
0888             while (from > 0) {
0889                 from--;
0890                 from = chunk.lastIndexOf(encodedEnter, static_cast<int>(from));
0891                 if (from == -1) {
0892                     from = 0;
0893                     break;
0894                 }
0895                 const qint64 backRef = std::max(from - 20, 0LL);
0896                 const qint64 size = from - backRef + static_cast<qint64>(encodedEnter.size());
0897                 QString decoded = _lister->codec()->toUnicode(chunk.mid(static_cast<int>(backRef), static_cast<int>(size)));
0898                 if (decoded.endsWith(QLatin1String("\n"))) {
0899                     if (from < (maxSize - encodedEnter.size())) {
0900                         qint64 arrayStart = from + encodedEnter.size();
0901                         decoded = _lister->codec()->toUnicode(chunk.mid(static_cast<int>(arrayStart), static_cast<int>(lastEnter - arrayStart)));
0902                         sizeY -= ((decoded.length() / (_sizeX + 1)) + 1);
0903                         if (sizeY < 0) {
0904                             from = arrayStart;
0905                             break;
0906                         }
0907                     }
0908                     lastEnter = from;
0909                 }
0910             }
0911 
0912             qint64 searchPos = readPos + from;
0913             QList<qint64> locs;
0914             while (searchPos < _screenStartPos) {
0915                 locs << searchPos;
0916                 readLines(searchPos, searchPos, 1);
0917             }
0918 
0919             if (locs.count() >= _sizeY) {
0920                 _screenStartPos = locs[locs.count() - _sizeY];
0921             } else if (from != 0) {
0922                 origSizeY += locs.count() + 1;
0923                 sizeY = origSizeY;
0924                 readNext = true;
0925             } else if (readPos == 0) {
0926                 _screenStartPos = 0;
0927             }
0928         }
0929 
0930     } break;
0931     case QAbstractSlider::SliderToMinimum:
0932         _screenStartPos = 0;
0933         break;
0934     case QAbstractSlider::SliderToMaximum:
0935         _screenStartPos = _lastPageStartPos;
0936         break;
0937     case QAbstractSlider::SliderMove: {
0938         if (_inSliderOp) // self created call?
0939             return;
0940         qint64 pos = _lister->scrollBar()->sliderPosition();
0941 
0942         if (pos == SLIDER_MAX) {
0943             _screenStartPos = _lastPageStartPos;
0944             break;
0945         } else if (pos == 0) {
0946             _screenStartPos = 0;
0947             break;
0948         }
0949 
0950         if (_lastPageStartPos > SLIDER_MAX)
0951             pos = _lastPageStartPos * pos / SLIDER_MAX;
0952 
0953         if (pos != 0) {
0954             if (_hexMode) {
0955                 const int bytesPerRow = _lister->hexBytesPerLine(_sizeX);
0956                 pos = (pos / bytesPerRow) * bytesPerRow;
0957             } else {
0958                 const int maxSize = _sizeX * _sizeY * MAX_CHAR_LENGTH;
0959                 qint64 readPos = pos - maxSize;
0960                 if (readPos < 0)
0961                     readPos = 0;
0962                 qint64 previousPos = readPos;
0963 
0964                 while (readPos <= pos) {
0965                     previousPos = readPos;
0966                     readLines(readPos, readPos, 1);
0967                 }
0968 
0969                 pos = previousPos;
0970             }
0971         }
0972 
0973         _screenStartPos = pos;
0974     } break;
0975     case QAbstractSlider::SliderNoAction:
0976         break;
0977     };
0978 
0979     _inSliderOp = true;
0980     const int value = static_cast<int>((_lastPageStartPos > SLIDER_MAX) ? SLIDER_MAX * _screenStartPos / _lastPageStartPos : _screenStartPos);
0981     _lister->scrollBar()->setSliderPosition(value);
0982     _inSliderOp = false;
0983 
0984     redrawTextArea();
0985 }
0986 
0987 void ListerTextArea::redrawTextArea(bool forcedUpdate)
0988 {
0989     if (_redrawing) {
0990         return;
0991     }
0992     _redrawing = true;
0993     bool isfirst;
0994     const qint64 pos = getCursorPosition(isfirst);
0995     calculateText(forcedUpdate);
0996     setCursorPositionInDocument(pos, isfirst);
0997     _redrawing = false;
0998 }
0999 
1000 void ListerTextArea::ensureVisibleCursor()
1001 {
1002     if (_screenStartPos <= _cursorPos && _cursorPos <= _screenEndPos) {
1003         return;
1004     }
1005 
1006     int delta = _sizeY / 2;
1007     if (delta == 0)
1008         delta++;
1009 
1010     qint64 newScreenStart = _cursorPos;
1011     while (delta) {
1012         const int maxSize = _sizeX * MAX_CHAR_LENGTH;
1013         qint64 readPos = newScreenStart - maxSize;
1014         if (readPos < 0)
1015             readPos = 0;
1016 
1017         qint64 previousPos = readPos;
1018 
1019         while (readPos < newScreenStart) {
1020             previousPos = readPos;
1021             readLines(readPos, readPos, 1);
1022             if (readPos == previousPos)
1023                 break;
1024         }
1025 
1026         newScreenStart = previousPos;
1027         delta--;
1028     }
1029     if (newScreenStart > _lastPageStartPos) {
1030         newScreenStart = _lastPageStartPos;
1031     }
1032 
1033     _screenStartPos = newScreenStart;
1034     slotActionTriggered(QAbstractSlider::SliderNoAction);
1035 }
1036 
1037 void ListerTextArea::setAnchorAndCursor(qint64 anchor, qint64 cursor)
1038 {
1039     _cursorPos = cursor;
1040     _cursorAnchorPos = anchor;
1041     ensureVisibleCursor();
1042     setCursorPositionInDocument(cursor, false);
1043 }
1044 
1045 QString ListerTextArea::getSelectedText()
1046 {
1047     if (_cursorAnchorPos != -1 && _cursorAnchorPos != _cursorPos) {
1048         return readSection(_cursorAnchorPos, _cursorPos);
1049     }
1050     return QString();
1051 }
1052 
1053 void ListerTextArea::copySelectedToClipboard()
1054 {
1055     const QString selection = getSelectedText();
1056     if (!selection.isEmpty()) {
1057         QApplication::clipboard()->setText(selection);
1058     }
1059 }
1060 
1061 void ListerTextArea::clearSelection()
1062 {
1063     QTextCursor cursor = textCursor();
1064     cursor.clearSelection();
1065     setTextCursor(cursor);
1066     _cursorAnchorPos = -1;
1067 }
1068 
1069 void ListerTextArea::performAnchorChange(int anchor)
1070 {
1071     int x, y;
1072     bool isfirst;
1073     getScreenPosition(anchor, x, y);
1074     _cursorAnchorPos = textToFilePositionOnScreen(x, y, isfirst);
1075 }
1076 
1077 void ListerTextArea::handleAnchorChange(int oldAnchor)
1078 {
1079     const int anchor = textCursor().anchor();
1080 
1081     if (oldAnchor != anchor) {
1082         performAnchorChange(anchor);
1083     }
1084 }
1085 
1086 void ListerTextArea::setHexMode(bool hexMode)
1087 {
1088     bool isfirst;
1089     const qint64 pos = getCursorPosition(isfirst);
1090     _hexMode = hexMode;
1091     _screenStartPos = 0;
1092     calculateText(true);
1093     setCursorPositionInDocument(pos, isfirst);
1094     ensureVisibleCursor();
1095 }
1096 
1097 void ListerTextArea::zoomIn(int range)
1098 {
1099     KTextEdit::zoomIn(range);
1100     redrawTextArea();
1101 }
1102 
1103 void ListerTextArea::zoomOut(int range)
1104 {
1105     KTextEdit::zoomOut(range);
1106     redrawTextArea();
1107 }
1108 
1109 ListerPane::ListerPane(Lister *lister, QWidget *parent)
1110     : QWidget(parent)
1111     , _lister(lister)
1112 {
1113 }
1114 
1115 bool ListerPane::event(QEvent *e)
1116 {
1117     const bool handled = ListerPane::handleCloseEvent(e);
1118     if (!handled) {
1119         return QWidget::event(e);
1120     }
1121     return true;
1122 }
1123 
1124 bool ListerPane::handleCloseEvent(QEvent *e)
1125 {
1126     if (e->type() == QEvent::ShortcutOverride) {
1127         auto *ke = dynamic_cast<QKeyEvent *>(e);
1128         if (ke->key() == Qt::Key_Escape) {
1129             if (_lister->isSearchEnabled()) {
1130                 _lister->searchDelete();
1131                 _lister->enableSearch(false);
1132                 ke->accept();
1133                 return true;
1134             }
1135             if (!_lister->textArea()->getSelectedText().isEmpty()) {
1136                 _lister->textArea()->clearSelection();
1137                 ke->accept();
1138                 return true;
1139             }
1140         }
1141     }
1142     return false;
1143 }
1144 
1145 ListerBrowserExtension::ListerBrowserExtension(Lister *lister)
1146     : KParts::BrowserExtension(lister)
1147 {
1148     _lister = lister;
1149 
1150     emit enableAction("copy", true);
1151     emit enableAction("print", true);
1152 }
1153 
1154 void ListerBrowserExtension::copy()
1155 {
1156     _lister->textArea()->copySelectedToClipboard();
1157 }
1158 
1159 void ListerBrowserExtension::print()
1160 {
1161     _lister->print();
1162 }
1163 
1164 class ListerEncodingMenu : public KrRemoteEncodingMenu
1165 {
1166 public:
1167     ListerEncodingMenu(Lister *lister, const QString &text, const QString &icon, KActionCollection *parent)
1168         : KrRemoteEncodingMenu(text, icon, parent)
1169         , _lister(lister)
1170     {
1171     }
1172 
1173 protected:
1174     QString currentCharacterSet() override
1175     {
1176         return _lister->characterSet();
1177     }
1178 
1179     void chooseDefault() override
1180     {
1181         _lister->setCharacterSet(QString());
1182     }
1183 
1184     void chooseEncoding(QString encodingName) override
1185     {
1186         QString charset = KCharsets::charsets()->encodingForName(encodingName);
1187         _lister->setCharacterSet(charset);
1188     }
1189 
1190     Lister *_lister;
1191 };
1192 
1193 Lister::Lister(QWidget *parent)
1194     : KParts::ReadOnlyPart(parent)
1195 {
1196     setXMLFile("krusaderlisterui.rc");
1197 
1198     _actionSaveSelected = new QAction(Icon("document-save"), i18n("Save selection..."), this);
1199     connect(_actionSaveSelected, &QAction::triggered, this, &Lister::saveSelected);
1200     actionCollection()->addAction("save_selected", _actionSaveSelected);
1201 
1202     _actionSaveAs = new QAction(Icon("document-save-as"), i18n("Save as..."), this);
1203     connect(_actionSaveAs, &QAction::triggered, this, &Lister::saveAs);
1204     actionCollection()->addAction("save_as", _actionSaveAs);
1205 
1206     _actionPrint = new QAction(Icon("document-print"), i18n("Print..."), this);
1207     connect(_actionPrint, &QAction::triggered, this, &Lister::print);
1208     actionCollection()->addAction("print", _actionPrint);
1209     actionCollection()->setDefaultShortcut(_actionPrint, Qt::CTRL + Qt::Key_P);
1210 
1211     _actionSearch = new QAction(Icon("system-search"), i18n("Search"), this);
1212     connect(_actionSearch, &QAction::triggered, this, &Lister::searchAction);
1213     actionCollection()->addAction("search", _actionSearch);
1214     actionCollection()->setDefaultShortcut(_actionSearch, Qt::CTRL + Qt::Key_F);
1215 
1216     _actionSearchNext = new QAction(Icon("go-down"), i18n("Search next"), this);
1217     connect(_actionSearchNext, &QAction::triggered, this, &Lister::searchNext);
1218     actionCollection()->addAction("search_next", _actionSearchNext);
1219     actionCollection()->setDefaultShortcut(_actionSearchNext, Qt::Key_F3);
1220 
1221     _actionSearchPrev = new QAction(Icon("go-up"), i18n("Search previous"), this);
1222     connect(_actionSearchPrev, &QAction::triggered, this, &Lister::searchPrev);
1223     actionCollection()->addAction("search_prev", _actionSearchPrev);
1224     actionCollection()->setDefaultShortcut(_actionSearchPrev, Qt::SHIFT + Qt::Key_F3);
1225 
1226     _actionJumpToPosition = new QAction(Icon("go-jump"), i18n("Jump to position"), this);
1227     connect(_actionJumpToPosition, &QAction::triggered, this, &Lister::jumpToPosition);
1228     actionCollection()->addAction("jump_to_position", _actionJumpToPosition);
1229     actionCollection()->setDefaultShortcut(_actionJumpToPosition, Qt::CTRL + Qt::Key_G);
1230 
1231     _actionHexMode = new QAction(Icon("document-preview"), i18n("Hex mode"), this);
1232     connect(_actionHexMode, &QAction::triggered, this, &Lister::toggleHexMode);
1233     actionCollection()->addAction("hex_mode", _actionHexMode);
1234     actionCollection()->setDefaultShortcut(_actionHexMode, Qt::CTRL + Qt::Key_H);
1235 
1236     new ListerEncodingMenu(this, i18n("Select charset"), "character-set", actionCollection());
1237 
1238     QWidget *widget = new ListerPane(this, parent);
1239     widget->setFocusPolicy(Qt::StrongFocus);
1240     auto *grid = new QGridLayout(widget);
1241     _textArea = new ListerTextArea(this, widget);
1242     _textArea->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
1243     _textArea->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
1244     _textArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1245     _textArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1246     widget->setFocusProxy(_textArea);
1247     grid->addWidget(_textArea, 0, 0);
1248     _scrollBar = new QScrollBar(Qt::Vertical, widget);
1249     grid->addWidget(_scrollBar, 0, 1);
1250     _scrollBar->hide();
1251 
1252     QWidget *statusWidget = new QWidget(widget);
1253     auto *hbox = new QHBoxLayout(statusWidget);
1254 
1255     _listerLabel = new QLabel(i18n("Lister:"), statusWidget);
1256     hbox->addWidget(_listerLabel);
1257     _searchProgressBar = new QProgressBar(statusWidget);
1258     _searchProgressBar->setMinimum(0);
1259     _searchProgressBar->setMaximum(1000);
1260     _searchProgressBar->setValue(0);
1261     _searchProgressBar->hide();
1262     hbox->addWidget(_searchProgressBar);
1263 
1264     _searchStopButton = new QToolButton(statusWidget);
1265     _searchStopButton->setIcon(Icon("process-stop"));
1266     _searchStopButton->setToolTip(i18n("Stop search"));
1267     _searchStopButton->hide();
1268     connect(_searchStopButton, &QToolButton::clicked, this, &Lister::searchDelete);
1269     hbox->addWidget(_searchStopButton);
1270 
1271     _searchLineEdit = new KLineEdit(statusWidget);
1272     _searchLineEdit->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
1273 
1274     _originalBackground = _searchLineEdit->palette().color(QPalette::Base);
1275     _originalForeground = _searchLineEdit->palette().color(QPalette::Text);
1276 
1277     connect(_searchLineEdit, &KLineEdit::KLINEEDIT_RETURNKEYPRESSED, this, &Lister::searchNext);
1278     connect(_searchLineEdit, &KLineEdit::textChanged, this, &Lister::searchTextChanged);
1279 
1280     hbox->addWidget(_searchLineEdit);
1281     _searchNextButton = new QPushButton(Icon("go-down"), i18n("Next"), statusWidget);
1282     _searchNextButton->setToolTip(i18n("Jump to next match"));
1283     connect(_searchNextButton, &QPushButton::clicked, this, &Lister::searchNext);
1284     hbox->addWidget(_searchNextButton);
1285     _searchPrevButton = new QPushButton(Icon("go-up"), i18n("Previous"), statusWidget);
1286     _searchPrevButton->setToolTip(i18n("Jump to previous match"));
1287     connect(_searchPrevButton, &QPushButton::clicked, this, &Lister::searchPrev);
1288     hbox->addWidget(_searchPrevButton);
1289     _searchOptions = new QPushButton(i18n("Options"), statusWidget);
1290     _searchOptions->setToolTip(i18n("Modify search behavior"));
1291     auto *menu = new QMenu();
1292     _fromCursorAction = menu->addAction(i18n("From cursor"));
1293     _fromCursorAction->setCheckable(true);
1294     _fromCursorAction->setChecked(true);
1295     _caseSensitiveAction = menu->addAction(i18n("Case sensitive"));
1296     _caseSensitiveAction->setCheckable(true);
1297     _matchWholeWordsOnlyAction = menu->addAction(i18n("Match whole words only"));
1298     _matchWholeWordsOnlyAction->setCheckable(true);
1299     _regExpAction = menu->addAction(i18n("RegExp"));
1300     _regExpAction->setCheckable(true);
1301     _hexAction = menu->addAction(i18n("Hexadecimal"));
1302     _hexAction->setCheckable(true);
1303     _searchOptions->setMenu(menu);
1304 
1305     hbox->addWidget(_searchOptions);
1306 
1307     hbox->addItem(new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum));
1308 
1309     _statusLabel = new QLabel(statusWidget);
1310     hbox->addWidget(_statusLabel);
1311 
1312     grid->addWidget(statusWidget, 1, 0, 1, 2);
1313     setWidget(widget);
1314 
1315     connect(_scrollBar, &QScrollBar::actionTriggered, _textArea, &ListerTextArea::slotActionTriggered);
1316     connect(&_searchUpdateTimer, &QTimer::timeout, this, &Lister::slotUpdate);
1317 
1318     new ListerBrowserExtension(this);
1319     enableSearch(false);
1320 
1321     _tempFile = new QTemporaryFile(this);
1322     _tempFile->setFileTemplate(QDir::tempPath() + QLatin1String("/krusader_lister.XXXXXX"));
1323 }
1324 
1325 bool Lister::openUrl(const QUrl &listerUrl)
1326 {
1327     _downloading = false;
1328     setUrl(listerUrl);
1329 
1330     _fileSize = 0;
1331 
1332     if (listerUrl.isLocalFile()) {
1333         _filePath = listerUrl.path();
1334         if (!QFile::exists(_filePath))
1335             return false;
1336         _fileSize = getFileSize();
1337     } else {
1338         if (_tempFile->isOpen()) {
1339             _tempFile->close();
1340         }
1341         _tempFile->open();
1342 
1343         _filePath = _tempFile->fileName();
1344 
1345         KIO::TransferJob *downloadJob = KIO::get(listerUrl, KIO::NoReload, KIO::HideProgressInfo);
1346 
1347         connect(downloadJob, &KIO::TransferJob::data, this, [=](KIO::Job *, QByteArray array) {
1348             if (array.size() != 0) {
1349                 _tempFile->write(array);
1350             }
1351         });
1352         connect(downloadJob, &KIO::TransferJob::result, this, [=](KJob *job) {
1353             _tempFile->flush();
1354             if (job->error()) { /* any error occurred? */
1355                 auto *kioJob = qobject_cast<KIO::TransferJob *>(job);
1356                 KMessageBox::error(_textArea, i18n("Error reading file %1.", kioJob->url().toDisplayString(QUrl::PreferLocalFile)));
1357             }
1358             _downloading = false;
1359             _downloadUpdateTimer.stop();
1360             slotUpdate();
1361         });
1362         connect(&_downloadUpdateTimer, &QTimer::timeout, this, [&]() {
1363             slotUpdate();
1364         });
1365         _downloadUpdateTimer.start(500);
1366         _downloading = true;
1367     }
1368 
1369     // invalidate cache
1370     _cache.clear();
1371 
1372     _textArea->reset();
1373     emit started(nullptr);
1374     emit setWindowCaption(listerUrl.toDisplayString());
1375     emit completed();
1376     return true;
1377 }
1378 
1379 QByteArray Lister::cacheChunk(const qint64 filePos, const qint64 maxSize)
1380 {
1381     if (filePos >= _fileSize) {
1382         return QByteArray();
1383     }
1384 
1385     qint64 size = maxSize;
1386     if (_fileSize - filePos < size) {
1387         size = _fileSize - filePos;
1388     }
1389 
1390     if (!_cache.isEmpty() && (filePos >= _cachePos) && (filePos + size <= _cachePos + _cache.size())) {
1391         return _cache.mid(static_cast<int>(filePos - _cachePos), static_cast<int>(size));
1392     }
1393 
1394     const int negativeOffset = CACHE_SIZE * 2 / 5;
1395     qint64 cachePos = filePos - negativeOffset;
1396     if (cachePos < 0)
1397         cachePos = 0;
1398 
1399     QFile sourceFile(_filePath);
1400     if (!sourceFile.open(QIODevice::ReadOnly)) {
1401         return QByteArray();
1402     }
1403 
1404     if (!sourceFile.seek(cachePos)) {
1405         return QByteArray();
1406     }
1407 
1408     const QByteArray bytes = sourceFile.read(CACHE_SIZE);
1409     if (bytes.isEmpty()) {
1410         return bytes;
1411     }
1412 
1413     _cache = bytes;
1414     _cachePos = cachePos;
1415     const qint64 cacheRefIndex = filePos - _cachePos;
1416     qint64 newSize = bytes.size() - cacheRefIndex;
1417     if (newSize < size)
1418         size = newSize;
1419 
1420     return _cache.mid(static_cast<int>(cacheRefIndex), static_cast<int>(size));
1421 }
1422 
1423 qint64 Lister::getFileSize()
1424 {
1425     return QFile(_filePath).size();
1426 }
1427 
1428 void Lister::guiActivateEvent(KParts::GUIActivateEvent *event)
1429 {
1430     if (event->activated()) {
1431         slotUpdate();
1432         _textArea->redrawTextArea(true);
1433     } else {
1434         enableSearch(false);
1435     }
1436     KParts::ReadOnlyPart::guiActivateEvent(event);
1437 }
1438 
1439 void Lister::slotUpdate()
1440 {
1441     const qint64 oldSize = _fileSize;
1442     _fileSize = getFileSize();
1443     if (oldSize != _fileSize)
1444         _textArea->sizeChanged();
1445 
1446     int cursorX = 0, cursorY = 0;
1447     _textArea->getCursorPosition(cursorX, cursorY);
1448     bool isfirst = false;
1449     const qint64 cursor = _textArea->getCursorPosition(isfirst);
1450 
1451     const int percent = (_fileSize == 0) ? 0 : (int)((201 * cursor) / _fileSize / 2);
1452 
1453     const QString status = i18n("Column: %1, Position: %2 (%3, %4%)", cursorX, cursor, _fileSize, percent);
1454     _statusLabel->setText(status);
1455 
1456     if (_searchProgressCounter)
1457         _searchProgressCounter--;
1458 }
1459 
1460 bool Lister::isSearchEnabled()
1461 {
1462     return !_searchLineEdit->isHidden() || !_searchProgressBar->isHidden();
1463 }
1464 
1465 void Lister::enableSearch(const bool enable)
1466 {
1467     if (enable) {
1468         _listerLabel->setText(i18n("Search:"));
1469         _searchLineEdit->show();
1470         _searchNextButton->show();
1471         _searchPrevButton->show();
1472         _searchOptions->show();
1473         if (!_searchLineEdit->hasFocus()) {
1474             _searchLineEdit->setFocus();
1475             const QString selection = _textArea->getSelectedText();
1476             if (!selection.isEmpty()) {
1477                 _searchLineEdit->setText(selection);
1478             }
1479             _searchLineEdit->selectAll();
1480         }
1481     } else {
1482         _listerLabel->setText(i18n("Lister:"));
1483         _searchLineEdit->hide();
1484         _searchNextButton->hide();
1485         _searchPrevButton->hide();
1486         _searchOptions->hide();
1487         _textArea->setFocus();
1488     }
1489 }
1490 
1491 void Lister::searchNext()
1492 {
1493     search(true);
1494 }
1495 
1496 void Lister::searchPrev()
1497 {
1498     search(false);
1499 }
1500 
1501 void Lister::search(const bool forward, const bool restart)
1502 {
1503     _restartFromBeginning = restart;
1504     if (_searchInProgress || _searchLineEdit->text().isEmpty())
1505         return;
1506     if (_searchLineEdit->isHidden())
1507         enableSearch(true);
1508 
1509     _searchPosition = forward ? 0 : _fileSize;
1510     if (_fromCursorAction->isChecked()) {
1511         bool isfirst;
1512         qint64 cursor = _textArea->getCursorPosition(isfirst);
1513         if (cursor != 0 && !forward)
1514             cursor--;
1515         if (_searchLastFailedPosition == -1 || _searchLastFailedPosition != cursor)
1516             _searchPosition = cursor;
1517     }
1518     const bool caseSensitive = _caseSensitiveAction->isChecked();
1519     const bool matchWholeWord = _matchWholeWordsOnlyAction->isChecked();
1520     const bool regExp = _regExpAction->isChecked();
1521     const bool hex = _hexAction->isChecked();
1522 
1523     if (hex) {
1524         QString hexcontent = _searchLineEdit->text();
1525         hexcontent.remove(QLatin1String("0x"));
1526         hexcontent.remove(' ');
1527         hexcontent.remove('\t');
1528         hexcontent.remove('\n');
1529         hexcontent.remove('\r');
1530 
1531         _searchHexQuery = QByteArray();
1532 
1533         if (hexcontent.length() & 1) {
1534             setColor(false, false);
1535             return;
1536         }
1537 
1538         while (!hexcontent.isEmpty()) {
1539             const QString hexData = hexcontent.left(2);
1540             hexcontent = hexcontent.mid(2);
1541             bool ok = true;
1542             const int c = hexData.toUInt(&ok, 16);
1543             if (!ok) {
1544                 setColor(false, false);
1545                 return;
1546             }
1547             _searchHexQuery.push_back((char)c);
1548         }
1549     } else {
1550         _searchQuery.setContent(_searchLineEdit->text(), caseSensitive, matchWholeWord, codec()->name(), regExp);
1551     }
1552     _searchIsForward = forward;
1553     _searchHexadecimal = hex;
1554 
1555     QTimer::singleShot(0, this, &Lister::slotSearchMore);
1556     _searchInProgress = true;
1557     _searchProgressCounter = 3;
1558 
1559     enableActions(false);
1560 }
1561 
1562 void Lister::enableActions(const bool state)
1563 {
1564     _actionSearch->setEnabled(state);
1565     _actionSearchNext->setEnabled(state);
1566     _actionSearchPrev->setEnabled(state);
1567     _actionJumpToPosition->setEnabled(state);
1568     if (state) {
1569         _searchUpdateTimer.stop();
1570     } else {
1571         slotUpdate();
1572     }
1573 }
1574 
1575 void Lister::slotSearchMore()
1576 {
1577     if (!_searchInProgress)
1578         return;
1579 
1580     if (!_searchUpdateTimer.isActive()) {
1581         _searchUpdateTimer.start(200);
1582     }
1583 
1584     updateProgressBar();
1585     if (!_searchIsForward)
1586         _searchPosition--;
1587 
1588     if (_searchPosition < 0 || _searchPosition >= _fileSize) {
1589         if (_restartFromBeginning)
1590             resetSearchPosition();
1591         else {
1592             searchFailed();
1593             return;
1594         }
1595     }
1596 
1597     qint64 maxCacheSize = SEARCH_CACHE_CHARS;
1598     qint64 searchPos = _searchPosition;
1599     bool setPosition = true;
1600     if (!_searchIsForward) {
1601         qint64 origSearchPos = _searchPosition;
1602         searchPos -= maxCacheSize;
1603         if (searchPos <= 0) {
1604             searchPos = 0;
1605             _searchPosition = 0;
1606             setPosition = false;
1607         }
1608         qint64 diff = origSearchPos - searchPos;
1609         if (diff < maxCacheSize)
1610             maxCacheSize = diff;
1611     }
1612 
1613     const QByteArray chunk = cacheChunk(searchPos, maxCacheSize);
1614     if (chunk.isEmpty()) {
1615         searchFailed();
1616         return;
1617     }
1618 
1619     const qint64 chunkSize = chunk.size();
1620 
1621     qint64 foundAnchor = -1;
1622     qint64 foundCursor = -1;
1623     qint64 byteCounter = 0;
1624 
1625     if (_searchHexadecimal) {
1626         const int ndx = _searchIsForward ? chunk.indexOf(_searchHexQuery) : chunk.lastIndexOf(_searchHexQuery);
1627         if (chunkSize > _searchHexQuery.length()) {
1628             if (_searchIsForward) {
1629                 _searchPosition = searchPos + chunkSize;
1630                 if ((_searchPosition < _fileSize) && (chunkSize > _searchHexQuery.length()))
1631                     _searchPosition -= _searchHexQuery.length();
1632                 byteCounter = _searchPosition - searchPos;
1633             } else {
1634                 if (_searchPosition > 0)
1635                     _searchPosition += _searchHexQuery.length();
1636             }
1637         }
1638         if (ndx != -1) {
1639             foundAnchor = searchPos + ndx;
1640             foundCursor = foundAnchor + _searchHexQuery.length();
1641         }
1642     } else {
1643         qint64 rowStart = 0;
1644         QString row = "";
1645 
1646         QScopedPointer<QTextDecoder> decoder(_codec->makeDecoder());
1647 
1648         while (byteCounter < chunkSize) {
1649             const QString chr = decoder->toUnicode(chunk.mid(static_cast<int>(byteCounter++), 1));
1650             if (chr.isEmpty() && byteCounter < chunkSize) {
1651                 continue;
1652             }
1653 
1654             if (chr != "\n")
1655                 row += chr;
1656 
1657             if (chr == "\n" || row.length() >= SEARCH_MAX_ROW_LEN || byteCounter >= chunkSize) {
1658                 if (setPosition) {
1659                     _searchPosition = searchPos + byteCounter;
1660                     if (!_searchIsForward) {
1661                         _searchPosition++;
1662                         setPosition = false;
1663                     }
1664                 }
1665 
1666                 if (_searchQuery.checkLine(row, !_searchIsForward)) {
1667                     QByteArray cachedBuffer = chunk.mid(static_cast<int>(rowStart), static_cast<int>(chunkSize - rowStart));
1668 
1669                     QTextStream stream(&cachedBuffer);
1670                     stream.setCodec(_codec);
1671 
1672                     stream.read(_searchQuery.matchIndex());
1673                     foundAnchor = searchPos + rowStart + stream.pos();
1674 
1675                     stream.read(_searchQuery.matchLength());
1676                     foundCursor = searchPos + rowStart + stream.pos();
1677 
1678                     if (_searchIsForward)
1679                         break;
1680                 }
1681 
1682                 row = "";
1683                 rowStart = byteCounter;
1684             }
1685         }
1686     }
1687 
1688     if (foundAnchor != -1 && foundCursor != -1) {
1689         _textArea->setAnchorAndCursor(foundAnchor, foundCursor);
1690         searchSucceeded();
1691         return;
1692     }
1693 
1694     if (_searchIsForward && searchPos + byteCounter >= _fileSize) {
1695         if (_restartFromBeginning)
1696             resetSearchPosition();
1697         else {
1698             searchFailed();
1699             return;
1700         }
1701     } else if (_searchPosition <= 0 || _searchPosition >= _fileSize) {
1702         if (_restartFromBeginning)
1703             resetSearchPosition();
1704         else {
1705             searchFailed();
1706             return;
1707         }
1708     }
1709 
1710     QTimer::singleShot(0, this, &Lister::slotSearchMore);
1711 }
1712 
1713 void Lister::resetSearchPosition()
1714 {
1715     _restartFromBeginning = false;
1716     _searchPosition = _searchIsForward ? 0 : _fileSize - 1;
1717 }
1718 
1719 void Lister::searchSucceeded()
1720 {
1721     _searchInProgress = false;
1722     setColor(true, false);
1723     hideProgressBar();
1724     _searchLastFailedPosition = -1;
1725 
1726     enableActions(true);
1727 }
1728 
1729 void Lister::searchFailed()
1730 {
1731     _searchInProgress = false;
1732     setColor(false, false);
1733     hideProgressBar();
1734     bool isfirst;
1735     _searchLastFailedPosition = _textArea->getCursorPosition(isfirst);
1736     if (!_searchIsForward)
1737         _searchLastFailedPosition--;
1738 
1739     enableActions(true);
1740 }
1741 
1742 void Lister::searchDelete()
1743 {
1744     _searchInProgress = false;
1745     setColor(false, true);
1746     hideProgressBar();
1747     _searchLastFailedPosition = -1;
1748 
1749     enableActions(true);
1750 }
1751 
1752 void Lister::searchTextChanged()
1753 {
1754     searchDelete();
1755     if (_fileSize < 0x10000) { // autosearch files less than 64k
1756         if (!_searchLineEdit->text().isEmpty()) {
1757             bool isfirst;
1758             const qint64 anchor = _textArea->getCursorAnchor();
1759             const qint64 cursor = _textArea->getCursorPosition(isfirst);
1760             if (cursor > anchor && anchor != -1) {
1761                 _textArea->setCursorPositionInDocument(anchor, true);
1762             }
1763             search(true, true);
1764         }
1765     }
1766 }
1767 
1768 void Lister::setColor(const bool match, const bool restore)
1769 {
1770     QColor fore, back;
1771 
1772     if (!restore) {
1773         const KConfigGroup gc(krConfig, "Colors");
1774 
1775         QString foreground, background;
1776         const QPalette p = QGuiApplication::palette();
1777 
1778         if (match) {
1779             foreground = "Quicksearch Match Foreground";
1780             background = "Quicksearch Match Background";
1781             fore = Qt::black;
1782             back = QColor(192, 255, 192);
1783         } else {
1784             foreground = "Quicksearch Non-match Foreground";
1785             background = "Quicksearch Non-match Background";
1786             fore = Qt::black;
1787             back = QColor(255, 192, 192);
1788         }
1789 
1790         if (gc.readEntry(foreground, QString()) == "KDE default")
1791             fore = p.color(QPalette::Active, QPalette::Text);
1792         else if (!gc.readEntry(foreground, QString()).isEmpty())
1793             fore = gc.readEntry(foreground, fore);
1794 
1795         if (gc.readEntry(background, QString()) == "KDE default")
1796             back = p.color(QPalette::Active, QPalette::Base);
1797         else if (!gc.readEntry(background, QString()).isEmpty())
1798             back = gc.readEntry(background, back);
1799     } else {
1800         back = _originalBackground;
1801         fore = _originalForeground;
1802     }
1803 
1804     QPalette pal = _searchLineEdit->palette();
1805     pal.setColor(QPalette::Base, back);
1806     pal.setColor(QPalette::Text, fore);
1807     _searchLineEdit->setPalette(pal);
1808 }
1809 
1810 void Lister::hideProgressBar()
1811 {
1812     if (!_searchProgressBar->isHidden()) {
1813         _searchProgressBar->hide();
1814         _searchStopButton->hide();
1815         _searchLineEdit->show();
1816         _searchNextButton->show();
1817         _searchPrevButton->show();
1818         _searchOptions->show();
1819         _listerLabel->setText(i18n("Search:"));
1820     }
1821 }
1822 
1823 void Lister::updateProgressBar()
1824 {
1825     if (_searchProgressCounter)
1826         return;
1827 
1828     if (_searchProgressBar->isHidden()) {
1829         _searchProgressBar->show();
1830         _searchStopButton->show();
1831         _searchOptions->hide();
1832         _searchLineEdit->hide();
1833         _searchNextButton->hide();
1834         _searchPrevButton->hide();
1835         _listerLabel->setText(i18n("Search position:"));
1836 
1837         // otherwise focus is set to document tab
1838         _textArea->setFocus();
1839     }
1840 
1841     const qint64 pcnt = (_fileSize == 0) ? 1000 : (2001 * _searchPosition) / _fileSize / 2;
1842     const auto pctInt = (int)pcnt;
1843     if (_searchProgressBar->value() != pctInt)
1844         _searchProgressBar->setValue(pctInt);
1845 }
1846 
1847 void Lister::jumpToPosition()
1848 {
1849     bool ok = true;
1850     QString res = QInputDialog::getText(_textArea, i18n("Jump to position"), i18n("Text position:"), QLineEdit::Normal, "0", &ok);
1851     if (!ok)
1852         return;
1853 
1854     res = res.trimmed();
1855     qint64 pos = -1;
1856     if (res.startsWith(QLatin1String("0x"))) {
1857         res = res.mid(2);
1858         bool ok;
1859         const qulonglong upos = res.toULongLong(&ok, 16);
1860         if (!ok) {
1861             KMessageBox::error(_textArea, i18n("Invalid number."), i18n("Jump to position"));
1862             return;
1863         }
1864         pos = (qint64)upos;
1865     } else {
1866         bool ok;
1867         const qulonglong upos = res.toULongLong(&ok);
1868         if (!ok) {
1869             KMessageBox::error(_textArea, i18n("Invalid number."), i18n("Jump to position"));
1870             return;
1871         }
1872         pos = (qint64)upos;
1873     }
1874 
1875     if (pos < 0 || pos > _fileSize) {
1876         KMessageBox::error(_textArea, i18n("Number out of range."), i18n("Jump to position"));
1877         return;
1878     }
1879 
1880     _textArea->deleteAnchor();
1881     _textArea->setCursorPositionInDocument(pos, true);
1882     _textArea->ensureVisibleCursor();
1883 }
1884 
1885 void Lister::saveAs()
1886 {
1887     const QUrl url = QFileDialog::getSaveFileUrl(_textArea, i18n("Lister"));
1888     if (url.isEmpty())
1889         return;
1890     QUrl sourceUrl;
1891     if (!_downloading)
1892         sourceUrl = QUrl::fromLocalFile(_filePath);
1893     else
1894         sourceUrl = this->url();
1895 
1896     QList<QUrl> urlList;
1897     urlList << sourceUrl;
1898 
1899     KIO::Job *job = KIO::copy(urlList, url);
1900     job->setUiDelegate(new KIO::JobUiDelegate());
1901     KIO::getJobTracker()->registerJob(job);
1902     job->uiDelegate()->setAutoErrorHandlingEnabled(true);
1903 }
1904 
1905 void Lister::saveSelected()
1906 {
1907     bool isfirst;
1908     const qint64 start = _textArea->getCursorAnchor();
1909     const qint64 end = _textArea->getCursorPosition(isfirst);
1910     if (start == -1 || start == end) {
1911         KMessageBox::error(_textArea, i18n("Nothing is selected."), i18n("Save selection..."));
1912         return;
1913     }
1914     if (start > end) {
1915         _savePosition = end;
1916         _saveEnd = start;
1917     } else {
1918         _savePosition = start;
1919         _saveEnd = end;
1920     }
1921 
1922     const QUrl url = QFileDialog::getSaveFileUrl(_textArea, i18n("Lister"));
1923     if (url.isEmpty())
1924         return;
1925 
1926     KIO::TransferJob *saveJob = KIO::put(url, -1, KIO::Overwrite);
1927     connect(saveJob, &KIO::TransferJob::dataReq, this, &Lister::slotDataSend);
1928     connect(saveJob, &KIO::TransferJob::result, this, &Lister::slotSendFinished);
1929 
1930     saveJob->setUiDelegate(new KIO::JobUiDelegate());
1931     KIO::getJobTracker()->registerJob(saveJob);
1932     saveJob->uiDelegate()->setAutoErrorHandlingEnabled(true);
1933 
1934     _actionSaveSelected->setEnabled(false);
1935 }
1936 
1937 void Lister::slotDataSend(KIO::Job *, QByteArray &array)
1938 {
1939     if (_savePosition >= _saveEnd) {
1940         array = QByteArray();
1941         return;
1942     }
1943     qint64 max = _saveEnd - _savePosition;
1944     if (max > 1000)
1945         max = 1000;
1946     array = cacheChunk(_savePosition, (int)max);
1947     _savePosition += array.size();
1948 }
1949 
1950 void Lister::slotSendFinished(KJob *)
1951 {
1952     _actionSaveSelected->setEnabled(true);
1953 }
1954 
1955 void Lister::setCharacterSet(const QString &set)
1956 {
1957     _characterSet = set;
1958     if (_characterSet.isEmpty()) {
1959         _codec = QTextCodec::codecForLocale();
1960     } else {
1961         _codec = KCharsets::charsets()->codecForName(_characterSet);
1962     }
1963     _textArea->redrawTextArea(true);
1964 }
1965 
1966 void Lister::print()
1967 {
1968     bool isfirst;
1969     const qint64 anchor = _textArea->getCursorAnchor();
1970     const qint64 cursor = _textArea->getCursorPosition(isfirst);
1971     const bool hasSelection = (anchor != -1 && anchor != cursor);
1972 
1973     const QString docName = url().fileName();
1974     QPrinter printer;
1975     printer.setDocName(docName);
1976 
1977     QScopedPointer<QPrintDialog> printDialog(new QPrintDialog(&printer, _textArea));
1978 
1979     if (hasSelection) {
1980         printDialog->addEnabledOption(QAbstractPrintDialog::PrintSelection);
1981     }
1982 
1983     if (!printDialog->exec()) {
1984         return;
1985     }
1986 
1987     if (printer.pageOrder() == QPrinter::LastPageFirst) {
1988         switch (KMessageBox::warningContinueCancel(_textArea, i18n("Reverse printing is not supported. Continue with normal printing?"))) {
1989         case KMessageBox::Continue:
1990             break;
1991         default:
1992             return;
1993         }
1994     }
1995     QPainter painter;
1996     painter.begin(&printer);
1997 
1998     const QString dateString = QDate::currentDate().toString(Qt::SystemLocaleShortDate);
1999 
2000 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
2001     const QRect pageRect = printer.pageLayout().paintRectPixels(printer.resolution());
2002 #else
2003     const QRect pageRect = printer.pageRect();
2004 #endif
2005     const QRect drawingRect(0, 0, pageRect.width(), pageRect.height());
2006 
2007     const QFont normalFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
2008     const QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
2009 
2010     const QFontMetrics fmNormal(normalFont);
2011     const int normalFontHeight = fmNormal.height();
2012 
2013     const QFontMetrics fmFixed(fixedFont);
2014     const int fixedFontHeight = std::max(fmFixed.height(), 1);
2015     const int fixedFontWidth = std::max(fmFixed.horizontalAdvance("W"), 1);
2016 
2017     const int effPageSize = drawingRect.height() - normalFontHeight - 1;
2018     const int rowsPerPage = std::max(effPageSize / fixedFontHeight, 1);
2019     const int columnsPerPage = std::max(drawingRect.width() / fixedFontWidth, 1);
2020 
2021     bool firstPage = true;
2022 
2023     qint64 startPos = 0;
2024     qint64 endPos = _fileSize;
2025     if (printer.printRange() == QPrinter::Selection) {
2026         if (anchor > cursor)
2027             startPos = cursor, endPos = anchor;
2028         else
2029             startPos = anchor, endPos = cursor;
2030     }
2031 
2032     int page = 0;
2033     while (startPos < endPos) {
2034         page++;
2035 
2036         QStringList rows = readLines(startPos, endPos, columnsPerPage, rowsPerPage);
2037 
2038         // print since set-up fromPage number
2039         if (printer.fromPage() && page < printer.fromPage()) {
2040             continue;
2041         }
2042 
2043         // print until set-up toPage number
2044         if (printer.toPage() && printer.toPage() >= printer.fromPage() && page > printer.toPage())
2045             break;
2046 
2047         if (!firstPage) {
2048             printer.newPage();
2049         }
2050         firstPage = false;
2051         // Use the painter to draw on the page.
2052         painter.setFont(normalFont);
2053 
2054         painter.drawText(drawingRect, Qt::AlignLeft, dateString);
2055         painter.drawText(drawingRect, Qt::AlignHCenter, docName);
2056         painter.drawText(drawingRect, Qt::AlignRight, QString("%1").arg(page));
2057 
2058         painter.drawLine(0, normalFontHeight, drawingRect.width(), normalFontHeight);
2059 
2060         painter.setFont(fixedFont);
2061         int yOffset = normalFontHeight + 1;
2062         foreach (const QString &row, rows) {
2063             painter.drawText(0, yOffset + fixedFontHeight, row);
2064             yOffset += fixedFontHeight;
2065         }
2066     }
2067 }
2068 
2069 QStringList Lister::readLines(qint64 &filePos, const qint64 endPos, const int columns, const int lines)
2070 {
2071     if (_textArea->hexMode()) {
2072         return readHexLines(filePos, endPos, columns, lines);
2073     }
2074     QStringList list;
2075     const int maxBytes = std::min(columns * lines * MAX_CHAR_LENGTH, (int)(endPos - filePos));
2076     if (maxBytes <= 0) {
2077         return list;
2078     }
2079     const QByteArray chunk = cacheChunk(filePos, maxBytes);
2080     if (chunk.isEmpty()) {
2081         return list;
2082     }
2083 
2084     QScopedPointer<QTextDecoder> decoder(_codec->makeDecoder());
2085 
2086     int byteCounter = 0;
2087     QString row = "";
2088     bool skipImmediateNewline = false;
2089     while (byteCounter < chunk.size() && list.size() < lines) {
2090         QString chr = decoder->toUnicode(chunk.mid(byteCounter++, 1));
2091         if (chr.isEmpty()) {
2092             continue;
2093         }
2094 
2095         // replace unreadable characters
2096         if ((chr[0] < 32) && (chr[0] != '\n') && (chr[0] != '\t')) {
2097             chr = QChar(' ');
2098         }
2099 
2100         // handle newline
2101         if (chr == "\n") {
2102             if (!skipImmediateNewline) {
2103                 list << row;
2104                 row = "";
2105             }
2106             skipImmediateNewline = false;
2107             continue;
2108         }
2109 
2110         skipImmediateNewline = false;
2111 
2112         // handle tab
2113         if (chr == "\t") {
2114             const int tabLength = _textArea->tabWidth() - (row.length() % _textArea->tabWidth());
2115             if (row.length() + tabLength > columns) {
2116                 list << row;
2117                 row = "";
2118             }
2119             row += QString(tabLength, QChar(' '));
2120         } else {
2121             // normal printable character
2122             row += chr;
2123         }
2124 
2125         if (row.length() >= columns) {
2126             list << row;
2127             row = "";
2128             skipImmediateNewline = true;
2129         }
2130     }
2131 
2132     if (list.size() < lines) {
2133         list << row;
2134     }
2135 
2136     filePos += byteCounter;
2137 
2138     return list;
2139 }
2140 
2141 int Lister::hexPositionDigits()
2142 {
2143     int positionDigits = 0;
2144     qint64 checker = _fileSize;
2145     while (checker) {
2146         positionDigits++;
2147         checker /= 16;
2148     }
2149     if (positionDigits < 8) {
2150         return 8;
2151     }
2152     return positionDigits;
2153 }
2154 
2155 int Lister::hexBytesPerLine(const int columns)
2156 {
2157     const int positionDigits = hexPositionDigits();
2158     if (columns >= positionDigits + 5 + 128) {
2159         return 32;
2160     }
2161     if (columns >= positionDigits + 5 + 64) {
2162         return 16;
2163     }
2164     return 8;
2165 }
2166 
2167 QStringList Lister::readHexLines(qint64 &filePos, const qint64 endPos, const int columns, const int lines)
2168 {
2169     const int positionDigits = hexPositionDigits();
2170     const int bytesPerRow = hexBytesPerLine(columns);
2171 
2172     QStringList list;
2173 
2174     const qint64 choppedPos = (filePos / bytesPerRow) * bytesPerRow;
2175     const int maxBytes = std::min(bytesPerRow * lines, (int)(endPos - choppedPos));
2176     if (maxBytes <= 0)
2177         return list;
2178 
2179     const QByteArray chunk = cacheChunk(choppedPos, maxBytes);
2180     if (chunk.isEmpty())
2181         return list;
2182 
2183     int cnt = 0;
2184     for (int l = 0; l < lines; l++) {
2185         if (filePos >= endPos) {
2186             break;
2187         }
2188 
2189         const qint64 printPos = (filePos / bytesPerRow) * bytesPerRow;
2190         QString pos;
2191         pos.setNum(printPos, 16);
2192         while (pos.length() < positionDigits)
2193             pos = QString("0") + pos;
2194         pos = QString("0x") + pos;
2195         pos += QString(": ");
2196 
2197         QString charData;
2198 
2199         for (int i = 0; i != bytesPerRow; ++i, ++cnt) {
2200             const qint64 currentPos = printPos + i;
2201             if (currentPos < filePos || currentPos >= endPos) {
2202                 pos += QString("   ");
2203                 charData += QString(" ");
2204             } else {
2205                 char c = chunk.at(cnt);
2206                 auto charCode = (int)c;
2207                 if (charCode < 0)
2208                     charCode += 256;
2209                 QString hex;
2210                 hex.setNum(charCode, 16);
2211                 if (hex.length() < 2)
2212                     hex = QString("0") + hex;
2213                 pos += hex + QString(" ");
2214                 if (c < 32)
2215                     c = '.';
2216                 charData += QChar(c);
2217             }
2218         }
2219 
2220         pos += QString(" ") + charData;
2221         list << pos;
2222         filePos = printPos + bytesPerRow;
2223     }
2224 
2225     if (filePos > endPos) {
2226         filePos = endPos;
2227     }
2228 
2229     return list;
2230 }
2231 
2232 int Lister::hexIndexToPosition(const int columns, const int index)
2233 {
2234     const int positionDigits = hexPositionDigits();
2235     const int bytesPerRow = hexBytesPerLine(columns);
2236     const int finalIndex = std::min(index, bytesPerRow);
2237 
2238     return positionDigits + 4 + (3 * finalIndex);
2239 }
2240 
2241 int Lister::hexPositionToIndex(const int columns, const int position)
2242 {
2243     const int positionDigits = hexPositionDigits();
2244     const int bytesPerRow = hexBytesPerLine(columns);
2245 
2246     int finalPosition = position;
2247     finalPosition -= 4 + positionDigits;
2248     if (finalPosition <= 0)
2249         return 0;
2250 
2251     finalPosition /= 3;
2252     if (finalPosition >= bytesPerRow)
2253         return bytesPerRow;
2254     return finalPosition;
2255 }
2256 
2257 void Lister::toggleHexMode()
2258 {
2259     setHexMode(!_textArea->hexMode());
2260 }
2261 
2262 void Lister::setHexMode(const bool mode)
2263 {
2264     if (mode) {
2265         _textArea->setHexMode(true);
2266         _actionHexMode->setText(i18n("Text mode"));
2267     } else {
2268         _textArea->setHexMode(false);
2269         _actionHexMode->setText(i18n("Hex mode"));
2270     }
2271 }