From c7b76823dac770da072667877622428591ac26a1 Mon Sep 17 00:00:00 2001 From: Soeren Apel Date: Sat, 30 Nov 2019 00:44:57 +0100 Subject: [PATCH] Add QHexView --- CMakeLists.txt | 2 + README | 12 +- pv/views/decoder_output/QHexView.cpp | 508 +++++++++++++++++++++++++++ pv/views/decoder_output/QHexView.hpp | 94 +++++ test/CMakeLists.txt | 2 + 5 files changed, 615 insertions(+), 3 deletions(-) create mode 100644 pv/views/decoder_output/QHexView.cpp create mode 100644 pv/views/decoder_output/QHexView.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c5824d32..fbfb5eb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -387,6 +387,7 @@ if(ENABLE_DECODE) pv/subwindows/decoder_selector/model.cpp pv/subwindows/decoder_selector/subwindow.cpp pv/views/decoder_output/view.cpp + pv/views/decoder_output/QHexView.cpp pv/views/trace/decodetrace.cpp pv/widgets/decodergroupbox.cpp pv/widgets/decodermenu.cpp @@ -396,6 +397,7 @@ if(ENABLE_DECODE) pv/data/decodesignal.hpp pv/subwindows/decoder_selector/subwindow.hpp pv/views/decoder_output/view.hpp + pv/views/decoder_output/QHexView.hpp pv/views/trace/decodetrace.hpp pv/widgets/decodergroupbox.hpp pv/widgets/decodermenu.hpp diff --git a/README b/README index 72fc5de5..a35cb8d5 100644 --- a/README +++ b/README @@ -22,9 +22,9 @@ PulseView is licensed under the terms of the GNU General Public License (GPL), version 3 or later. While some individual source code files are licensed under the GPLv2+, and -some files are licensed under the GPLv3+, this doesn't change the fact that -the program as a whole is licensed under the terms of the GPLv3+ (e.g. also -due to the fact that it links against GPLv3+ libraries). +some files are licensed under the GPLv3+ or MIT, this doesn't change the fact +that the program as a whole is licensed under the terms of the GPLv3+ (e.g. +also due to the fact that it links against GPLv3+ libraries). Please see the individual source files for the full list of copyright holders. @@ -83,6 +83,12 @@ DarkStyle: Juergen Skrotzky MIT license https://github.com/Jorgen-VikingGod/Qt-Frameless-Window-DarkStyle#licence +QHexView: + https://github.com/virinext/QHexView + License: + MIT license + https://github.com/virinext/QHexView/blob/master/LICENSE + Mailing list ------------ diff --git a/pv/views/decoder_output/QHexView.cpp b/pv/views/decoder_output/QHexView.cpp new file mode 100644 index 00000000..7d43cf0c --- /dev/null +++ b/pv/views/decoder_output/QHexView.cpp @@ -0,0 +1,508 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2015 Victor Anjin + * Copyright (C) 2019 Soeren Apel + * + * The MIT License (MIT) + * + * Copyright (c) 2015 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "QHexView.hpp" + +using std::size_t; + +const unsigned int HEXCHARS_IN_LINE = 47; +const unsigned int GAP_ADR_HEX = 10; +const unsigned int GAP_HEX_ASCII = 16; +const unsigned int BYTES_PER_LINE = 16; + + +DataStorageArray::DataStorageArray(const QByteArray &arr) +{ + data_ = arr; +} + +QByteArray DataStorageArray::getData(size_t position, size_t length) +{ + return data_.mid(position, length); +} + +size_t DataStorageArray::size() +{ + return data_.count(); +} + + + +QHexView::QHexView(QWidget *parent): + QAbstractScrollArea(parent), + pdata_(nullptr) +{ + setFont(QFont("Courier", 10)); + + charWidth_ = fontMetrics().width(QLatin1Char('9')); + charHeight_ = fontMetrics().height(); + + posAddr_ = 0; + posHex_ = 10 * charWidth_ + GAP_ADR_HEX; + posAscii_ = posHex_ + HEXCHARS_IN_LINE * charWidth_ + GAP_HEX_ASCII; + + setMinimumWidth(posAscii_ + (BYTES_PER_LINE * charWidth_)); + + setFocusPolicy(Qt::StrongFocus); +} + +QHexView::~QHexView() +{ + if (pdata_) + delete pdata_; +} + +void QHexView::setData(DataStorage *pData) +{ + verticalScrollBar()->setValue(0); + + if (pdata_) + delete pdata_; + + pdata_ = pData; + cursorPos_ = 0; + resetSelection(0); +} + +void QHexView::showFromOffset(size_t offset) +{ + if (pdata_ && (offset < pdata_->size())) { + setCursorPos(offset * 2); + + int cursorY = cursorPos_ / (2 * BYTES_PER_LINE); + verticalScrollBar() -> setValue(cursorY); + } +} + +void QHexView::clear() +{ + verticalScrollBar()->setValue(0); +} + +QSize QHexView::fullSize() const +{ + if (!pdata_) + return QSize(0, 0); + + size_t width = posAscii_ + (BYTES_PER_LINE * charWidth_); + size_t height = pdata_->size() / BYTES_PER_LINE; + if (pdata_->size() % BYTES_PER_LINE) + height++; + + height *= charHeight_; + + return QSize(width, height); +} + +void QHexView::paintEvent(QPaintEvent *event) +{ + if (!pdata_) + return; + + QPainter painter(viewport()); + + QSize areaSize = viewport()->size(); + QSize widgetSize = fullSize(); + verticalScrollBar()->setPageStep(areaSize.height() / charHeight_); + verticalScrollBar()->setRange(0, (widgetSize.height() - areaSize.height()) / charHeight_ + 1); + + size_t firstLineIdx = verticalScrollBar()->value(); + + size_t lastLineIdx = firstLineIdx + areaSize.height() / charHeight_; + if (lastLineIdx > (pdata_->size() / BYTES_PER_LINE)) { + lastLineIdx = pdata_->size() / BYTES_PER_LINE; + if (pdata_->size() % BYTES_PER_LINE) + lastLineIdx++; + } + + painter.fillRect(event->rect(), this->palette().color(QPalette::Base)); + + QColor addressAreaColor = QColor(0xd4, 0xd4, 0xd4, 0xff); + painter.fillRect(QRect(posAddr_, event->rect().top(), + posHex_ - GAP_ADR_HEX + 2, height()), addressAreaColor); + + int linePos = posAscii_ - (GAP_HEX_ASCII / 2); + painter.setPen(Qt::gray); + + painter.drawLine(linePos, event->rect().top(), linePos, height()); + + painter.setPen(Qt::black); + + int yPosStart = charHeight_; + + QBrush def = painter.brush(); + QBrush selected = QBrush(QColor(0x6d, 0x9e, 0xff, 0xff)); + QByteArray data = pdata_->getData(firstLineIdx * BYTES_PER_LINE, (lastLineIdx - firstLineIdx) * BYTES_PER_LINE); + + for (size_t lineIdx = firstLineIdx, yPos = yPosStart; lineIdx < lastLineIdx; lineIdx++) { + QString address = QString("%1").arg(lineIdx * 16, 10, 16, QChar('0')); + painter.drawText(posAddr_, yPos, address); + + int xPos = posHex_; + for (size_t i = 0; i < BYTES_PER_LINE && ((lineIdx - firstLineIdx) * BYTES_PER_LINE + i) < (size_t)data.size(); i++) { + size_t pos = (lineIdx * BYTES_PER_LINE + i) * 2; + if ((pos >= selectBegin_) && (pos < selectEnd_)) { + painter.setBackground(selected); + painter.setBackgroundMode(Qt::OpaqueMode); + } + + QString val = QString::number((data.at((lineIdx - firstLineIdx) * BYTES_PER_LINE + i) & 0xF0) >> 4, 16); + painter.drawText(xPos, yPos, val); + + if (((pos+1) >= selectBegin_) && ((pos+1) < selectEnd_)) { + painter.setBackground(selected); + painter.setBackgroundMode(Qt::OpaqueMode); + } else { + painter.setBackground(def); + painter.setBackgroundMode(Qt::OpaqueMode); + } + + val = QString::number((data.at((lineIdx - firstLineIdx) * BYTES_PER_LINE + i) & 0xF), 16); + painter.drawText(xPos + charWidth_, yPos, val); + + painter.setBackground(def); + painter.setBackgroundMode(Qt::OpaqueMode); + + xPos += 3 * charWidth_; + } + + int xPosAscii = posAscii_; + for (size_t i = 0; ((lineIdx - firstLineIdx) * BYTES_PER_LINE + i) < (size_t)data.size() && (i < BYTES_PER_LINE); i++) { + char ch = data[(unsigned int)((lineIdx - firstLineIdx) * BYTES_PER_LINE + i)]; + + if ((ch < 0x20) || (ch > 0x7e)) + ch = '.'; + + painter.drawText(xPosAscii, yPos, QString(ch)); + xPosAscii += charWidth_; + } + + yPos += charHeight_; + } + + if (hasFocus()) { + int x = (cursorPos_ % (2 * BYTES_PER_LINE)); + int y = cursorPos_ / (2 * BYTES_PER_LINE); + y -= firstLineIdx; + int cursorX = (((x / 2) * 3) + (x % 2)) * charWidth_ + posHex_; + int cursorY = y * charHeight_ + 4; + painter.fillRect(cursorX, cursorY, 2, charHeight_, this->palette().color(QPalette::WindowText)); + } +} + +void QHexView::keyPressEvent(QKeyEvent *event) +{ + bool setVisible = false; + + /*****************************************************************************/ + /* Cursor movements */ + /*****************************************************************************/ + if (event->matches(QKeySequence::MoveToNextChar)) { + setCursorPos(cursorPos_ + 1); + resetSelection(cursorPos_); + setVisible = true; + } + if (event->matches(QKeySequence::MoveToPreviousChar)) { + setCursorPos(cursorPos_ - 1); + resetSelection(cursorPos_); + setVisible = true; + } + + if (event->matches(QKeySequence::MoveToEndOfLine)) { + setCursorPos(cursorPos_ | ((BYTES_PER_LINE * 2) - 1)); + resetSelection(cursorPos_); + setVisible = true; + } + if (event->matches(QKeySequence::MoveToStartOfLine)) { + setCursorPos(cursorPos_ | (cursorPos_ % (BYTES_PER_LINE * 2))); + resetSelection(cursorPos_); + setVisible = true; + } + if (event->matches(QKeySequence::MoveToPreviousLine)) { + setCursorPos(cursorPos_ - BYTES_PER_LINE * 2); + resetSelection(cursorPos_); + setVisible = true; + } + if (event->matches(QKeySequence::MoveToNextLine)) { + setCursorPos(cursorPos_ + BYTES_PER_LINE * 2); + resetSelection(cursorPos_); + setVisible = true; + } + + if (event->matches(QKeySequence::MoveToNextPage)) { + setCursorPos(cursorPos_ + (viewport()->height() / charHeight_ - 1) * 2 * BYTES_PER_LINE); + resetSelection(cursorPos_); + setVisible = true; + } + if (event->matches(QKeySequence::MoveToPreviousPage)) { + setCursorPos(cursorPos_ - (viewport()->height() / charHeight_ - 1) * 2 * BYTES_PER_LINE); + resetSelection(cursorPos_); + setVisible = true; + } + if (event->matches(QKeySequence::MoveToEndOfDocument)) { + if (pdata_) + setCursorPos(pdata_->size() * 2); + resetSelection(cursorPos_); + setVisible = true; + } + if (event->matches(QKeySequence::MoveToStartOfDocument)) { + setCursorPos(0); + resetSelection(cursorPos_); + setVisible = true; + } + + /*****************************************************************************/ + /* Select commands */ + /*****************************************************************************/ + if (event->matches(QKeySequence::SelectAll)) { + resetSelection(0); + if (pdata_) + setSelection(2 * pdata_->size() + 1); + setVisible = true; + } + if (event->matches(QKeySequence::SelectNextChar)) { + int pos = cursorPos_ + 1; + setCursorPos(pos); + setSelection(pos); + setVisible = true; + } + if (event->matches(QKeySequence::SelectPreviousChar)) { + int pos = cursorPos_ - 1; + setSelection(pos); + setCursorPos(pos); + setVisible = true; + } + if (event->matches(QKeySequence::SelectEndOfLine)) { + int pos = cursorPos_ - (cursorPos_ % (2 * BYTES_PER_LINE)) + (2 * BYTES_PER_LINE); + setCursorPos(pos); + setSelection(pos); + setVisible = true; + } + if (event->matches(QKeySequence::SelectStartOfLine)) { + int pos = cursorPos_ - (cursorPos_ % (2 * BYTES_PER_LINE)); + setCursorPos(pos); + setSelection(pos); + setVisible = true; + } + if (event->matches(QKeySequence::SelectPreviousLine)) { + int pos = cursorPos_ - (2 * BYTES_PER_LINE); + setCursorPos(pos); + setSelection(pos); + setVisible = true; + } + if (event->matches(QKeySequence::SelectNextLine)) { + int pos = cursorPos_ + (2 * BYTES_PER_LINE); + setCursorPos(pos); + setSelection(pos); + setVisible = true; + } + + if (event->matches(QKeySequence::SelectNextPage)) { + int pos = cursorPos_ + (((viewport()->height() / charHeight_) - 1) * 2 * BYTES_PER_LINE); + setCursorPos(pos); + setSelection(pos); + setVisible = true; + } + if (event->matches(QKeySequence::SelectPreviousPage)) { + int pos = cursorPos_ - (((viewport()->height() / charHeight_) - 1) * 2 * BYTES_PER_LINE); + setCursorPos(pos); + setSelection(pos); + setVisible = true; + } + if (event->matches(QKeySequence::SelectEndOfDocument)) { + int pos = 0; + if (pdata_) + pos = pdata_->size() * 2; + setCursorPos(pos); + setSelection(pos); + setVisible = true; + } + if (event->matches(QKeySequence::SelectStartOfDocument)) { + int pos = 0; + setCursorPos(pos); + setSelection(pos); + setVisible = true; + } + + if (event->matches(QKeySequence::Copy) && (pdata_)) { + QString text; + int idx = 0; + int copyOffset = 0; + + QByteArray data = pdata_->getData(selectBegin_ / 2, (selectEnd_ - selectBegin_) / 2 + 1); + if (selectBegin_ % 2) { + text += QString::number((data.at((idx+1) / 2) & 0xF), 16); + text += " "; + idx++; + copyOffset = 1; + } + + int selectedSize = selectEnd_ - selectBegin_; + while (idx < selectedSize) { + QString val = QString::number((data.at((copyOffset + idx) / 2) & 0xF0) >> 4, 16); + if ((idx + 1) < selectedSize) { + val += QString::number((data.at((copyOffset + idx) / 2) & 0xF), 16); + val += " "; + } + text += val; + + if ((idx/2) % BYTES_PER_LINE == (BYTES_PER_LINE - 1)) + text += "\n"; + + idx += 2; + } + + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(text); + if (clipboard->supportsSelection()) + clipboard->setText(text); + } + + if (setVisible) + ensureVisible(); + + viewport()->update(); +} + +void QHexView::mouseMoveEvent(QMouseEvent *event) +{ + int actPos = cursorPos(event->pos()); + setCursorPos(actPos); + setSelection(actPos); + + viewport()->update(); +} + +void QHexView::mousePressEvent(QMouseEvent *event) +{ + int cPos = cursorPos(event->pos()); + + if ((QApplication::keyboardModifiers() & Qt::ShiftModifier) && (event->button() == Qt::LeftButton)) + setSelection(cPos); + else + resetSelection(cPos); + + setCursorPos(cPos); + + viewport()->update(); +} + +size_t QHexView::cursorPos(const QPoint &position) +{ + int pos = -1; + + if (((size_t)position.x() >= posHex_) && + ((size_t)position.x() < (posHex_ + HEXCHARS_IN_LINE * charWidth_))) { + + int x = (position.x() - posHex_) / charWidth_; + + if ((x % 3) == 0) + x = (x / 3) * 2; + else + x = ((x / 3) * 2) + 1; + + int firstLineIdx = verticalScrollBar()->value(); + int y = (position.y() / charHeight_) * 2 * BYTES_PER_LINE; + pos = x + y + firstLineIdx * BYTES_PER_LINE * 2; + } + + return pos; +} + +void QHexView::resetSelection() +{ + selectBegin_ = selectInit_; + selectEnd_ = selectInit_; +} + +void QHexView::resetSelection(int pos) +{ + if (pos < 0) + pos = 0; + + selectInit_ = pos; + selectBegin_ = pos; + selectEnd_ = pos; +} + +void QHexView::setSelection(int pos) +{ + if (pos < 0) + pos = 0; + + if ((size_t)pos >= selectInit_) { + selectEnd_ = pos; + selectBegin_ = selectInit_; + } else { + selectBegin_ = pos; + selectEnd_ = selectInit_; + } +} + +void QHexView::setCursorPos(int position) +{ + if (position < 0) + position = 0; + + int maxPos = 0; + if (pdata_) { + maxPos = pdata_->size() * 2; + if (pdata_->size() % BYTES_PER_LINE) + maxPos++; + } + + if (position > maxPos) + position = maxPos; + + cursorPos_ = position; +} + +void QHexView::ensureVisible() +{ + QSize areaSize = viewport()->size(); + + int firstLineIdx = verticalScrollBar() -> value(); + int lastLineIdx = firstLineIdx + areaSize.height() / charHeight_; + + int cursorY = cursorPos_ / (2 * BYTES_PER_LINE); + + if (cursorY < firstLineIdx) + verticalScrollBar() -> setValue(cursorY); + else if(cursorY >= lastLineIdx) + verticalScrollBar() -> setValue(cursorY - areaSize.height() / charHeight_ + 1); +} diff --git a/pv/views/decoder_output/QHexView.hpp b/pv/views/decoder_output/QHexView.hpp new file mode 100644 index 00000000..fd4b86f1 --- /dev/null +++ b/pv/views/decoder_output/QHexView.hpp @@ -0,0 +1,94 @@ +/* + * This file is part of the PulseView project. + * + * Copyright (C) 2015 Victor Anjin + * Copyright (C) 2019 Soeren Apel + * + * The MIT License (MIT) + * + * Copyright (c) 2015 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef PULSEVIEW_PV_VIEWS_DECODEROUTPUT_QHEXVIEW_H +#define PULSEVIEW_PV_VIEWS_DECODEROUTPUT_QHEXVIEW_H + +#include +#include +#include + +using std::size_t; + + +class DataStorage +{ +public: + virtual ~DataStorage() {}; + virtual QByteArray getData(size_t position, size_t length) = 0; + virtual size_t size() = 0; +}; + + +class DataStorageArray: public DataStorage +{ +public: + DataStorageArray(const QByteArray &arr); + virtual QByteArray getData(size_t position, size_t length); + virtual size_t size(); + +private: + QByteArray data_; +}; + + +class QHexView: public QAbstractScrollArea +{ + QHexView(QWidget *parent = 0); + ~QHexView(); + +public Q_SLOTS: + void setData(DataStorage *pData); + void clear(); + void showFromOffset(size_t offset); + +protected: + void paintEvent(QPaintEvent *event); + void keyPressEvent(QKeyEvent *event); + void mouseMoveEvent(QMouseEvent *event); + void mousePressEvent(QMouseEvent *event); + +private: + QSize fullSize() const; + void resetSelection(); + void resetSelection(int pos); + void setSelection(int pos); + void ensureVisible(); + void setCursorPos(int pos); + size_t cursorPos(const QPoint &position); + +private: + DataStorage *pdata_; + + size_t posAddr_, posHex_, posAscii_; + size_t charWidth_, charHeight_; + size_t selectBegin_, selectEnd_, selectInit_, cursorPos_; +}; + +#endif /* PULSEVIEW_PV_VIEWS_DECODEROUTPUT_QHEXVIEW_H */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0f81a0e5..99c490f8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -173,6 +173,7 @@ if(ENABLE_DECODE) ${PROJECT_SOURCE_DIR}/pv/subwindows/decoder_selector/model.cpp ${PROJECT_SOURCE_DIR}/pv/subwindows/decoder_selector/subwindow.cpp ${PROJECT_SOURCE_DIR}/pv/views/decoder_output/view.cpp + ${PROJECT_SOURCE_DIR}/pv/views/decoder_output/QHexView.cpp ${PROJECT_SOURCE_DIR}/pv/views/trace/decodetrace.cpp ${PROJECT_SOURCE_DIR}/pv/widgets/decodergroupbox.cpp ${PROJECT_SOURCE_DIR}/pv/widgets/decodermenu.cpp @@ -182,6 +183,7 @@ if(ENABLE_DECODE) ${PROJECT_SOURCE_DIR}/pv/data/decodesignal.hpp ${PROJECT_SOURCE_DIR}/pv/subwindows/decoder_selector/subwindow.hpp ${PROJECT_SOURCE_DIR}/pv/views/decoder_output/view.hpp + ${PROJECT_SOURCE_DIR}/pv/views/decoder_output/QHexView.hpp ${PROJECT_SOURCE_DIR}/pv/views/trace/decodetrace.hpp ${PROJECT_SOURCE_DIR}/pv/widgets/decodergroupbox.hpp ${PROJECT_SOURCE_DIR}/pv/widgets/decodermenu.hpp -- 2.30.2