]> sigrok.org Git - pulseview.git/blob - pv/views/decoder_output/QHexView.cpp
DecodeOutputView: Use delayed view updater and cache current chunk
[pulseview.git] / pv / views / decoder_output / QHexView.cpp
1 /*
2  * This file is part of the PulseView project.
3  *
4  * Copyright (C) 2015 Victor Anjin <virinext@gmail.com>
5  * Copyright (C) 2019 Soeren Apel <soeren@apelpie.net>
6  *
7  * The MIT License (MIT)
8  *
9  * Copyright (c) 2015
10  *
11  * Permission is hereby granted, free of charge, to any person obtaining a copy
12  * of this software and associated documentation files (the "Software"), to deal
13  * in the Software without restriction, including without limitation the rights
14  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15  * copies of the Software, and to permit persons to whom the Software is
16  * furnished to do so, subject to the following conditions:
17  *
18  * The above copyright notice and this permission notice shall be included in all
19  * copies or substantial portions of the Software.
20  *
21  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27  * SOFTWARE.
28  */
29
30 #include <QApplication>
31 #include <QClipboard>
32 #include <QFont>
33 #include <QKeyEvent>
34 #include <QScrollBar>
35 #include <QSize>
36 #include <QPainter>
37 #include <QPaintEvent>
38
39 #include "QHexView.hpp"
40
41 const unsigned int BYTES_PER_LINE   = 16;
42 const unsigned int HEXCHARS_IN_LINE = BYTES_PER_LINE * 3 - 1;
43 const unsigned int GAP_ADR_HEX      = 10;
44 const unsigned int GAP_HEX_ASCII    = 10;
45 const unsigned int GAP_ASCII_SLIDER = 5;
46
47
48 QHexView::QHexView(QWidget *parent):
49         QAbstractScrollArea(parent),
50         mode_(ChunkedDataMode),
51         data_(nullptr),
52         selectBegin_(0),
53         selectEnd_(0),
54         cursorPos_(0)
55 {
56         setFont(QFont("Courier", 10));
57
58         charWidth_ = fontMetrics().boundingRect('X').width();
59         charHeight_ = fontMetrics().height();
60
61         // Determine X coordinates of the three sub-areas
62         posAddr_  = 0;
63         posHex_   = 10 * charWidth_ + GAP_ADR_HEX;
64         posAscii_ = posHex_ + HEXCHARS_IN_LINE * charWidth_ + GAP_HEX_ASCII;
65
66         setFocusPolicy(Qt::StrongFocus);
67
68         if (palette().color(QPalette::ButtonText).toHsv().value() > 127) {
69                 // Color is bright
70                 chunk_colors_.emplace_back(100, 149, 237); // QColorConstants::Svg::cornflowerblue
71                 chunk_colors_.emplace_back(60, 179, 113);  // QColorConstants::Svg::mediumseagreen
72                 chunk_colors_.emplace_back(210, 180, 140); // QColorConstants::Svg::tan
73         } else {
74                 // Color is dark
75                 chunk_colors_.emplace_back(0, 0, 139);   // QColorConstants::Svg::darkblue
76                 chunk_colors_.emplace_back(34, 139, 34); // QColorConstants::Svg::forestgreen
77                 chunk_colors_.emplace_back(160, 82, 45); // QColorConstants::Svg::sienna
78         }
79 }
80
81 void QHexView::setMode(Mode m)
82 {
83         mode_ = m;
84
85         // This is not expected to be set when data is showing,
86         // so we don't update the viewport here
87 }
88
89 void QHexView::setData(const DecodeBinaryClass* data)
90 {
91         data_ = data;
92
93         data_size_ = 0;
94         for (const DecodeBinaryDataChunk& chunk : data_->chunks)
95                 data_size_ += chunk.data.size();
96
97         viewport()->update();
98 }
99
100 void QHexView::clear()
101 {
102         verticalScrollBar()->setValue(0);
103         data_ = nullptr;
104         data_size_ = 0;
105
106         viewport()->update();
107 }
108
109 void QHexView::showFromOffset(size_t offset)
110 {
111         if (data_ && (offset < data_size_)) {
112                 setCursorPos(offset * 2);
113
114                 int cursorY = cursorPos_ / (2 * BYTES_PER_LINE);
115                 verticalScrollBar() -> setValue(cursorY);
116         }
117
118         viewport()->update();
119 }
120
121 void QHexView::initialize_byte_iterator(size_t offset)
122 {
123         current_chunk_id_ = 0;
124         current_chunk_offset_ = 0;
125         current_offset_ = offset;
126
127         for (const DecodeBinaryDataChunk& chunk : data_->chunks)
128                 if (offset >= chunk.data.size()) {
129                         current_chunk_id_++;
130                         offset -= chunk.data.size();
131                 } else {
132                         current_chunk_offset_ = offset;
133                         break;
134                 }
135
136         current_chunk_ = data_->chunks[current_chunk_id_];
137 }
138
139 uint8_t QHexView::get_next_byte(bool* is_next_chunk)
140 {
141         if (is_next_chunk != nullptr)
142                 *is_next_chunk = (current_chunk_offset_ == 0);
143
144         uint8_t v = current_chunk_.data[current_chunk_offset_];
145
146         current_offset_++;
147         current_chunk_offset_++;
148
149         if (current_chunk_offset_ == current_chunk_.data.size()) {
150                 current_chunk_id_++;
151                 current_chunk_offset_ = 0;
152                 current_chunk_ = data_->chunks[current_chunk_id_];
153         }
154
155         return v;
156 }
157
158 QSize QHexView::getFullSize() const
159 {
160         size_t width = posAscii_ + (BYTES_PER_LINE * charWidth_) +
161                 GAP_ASCII_SLIDER + verticalScrollBar()->width();
162
163         if (!data_)
164                 return QSize(width, 0);
165
166         size_t height = data_size_ / BYTES_PER_LINE;
167
168         if (data_size_ % BYTES_PER_LINE)
169                 height++;
170
171         height *= charHeight_;
172
173         return QSize(width, height);
174 }
175
176 void QHexView::paintEvent(QPaintEvent *event)
177 {
178         QPainter painter(viewport());
179
180         // Calculate and update the widget and paint area sizes
181         QSize widgetSize = getFullSize();
182         setMinimumWidth(widgetSize.width());
183         setMaximumWidth(widgetSize.width());
184         QSize areaSize = viewport()->size();
185
186         verticalScrollBar()->setPageStep(areaSize.height() / charHeight_);
187         verticalScrollBar()->setRange(0, (widgetSize.height() - areaSize.height()) / charHeight_ + 1);
188
189         // Fill widget background
190         painter.fillRect(event->rect(), palette().color(QPalette::Base));
191
192         if (!data_ || (data_size_ == 0)) {
193                 painter.setPen(palette().color(QPalette::Text));
194                 QString s = tr("No data available");
195                 int x = (areaSize.width() - fontMetrics().boundingRect(s).width()) / 2;
196                 int y = areaSize.height() / 2;
197                 painter.drawText(x, y, s);
198                 return;
199         }
200
201         // Determine first/last line indices
202         size_t firstLineIdx = verticalScrollBar()->value();
203
204         size_t lastLineIdx = firstLineIdx + areaSize.height() / charHeight_;
205         if (lastLineIdx > (data_size_ / BYTES_PER_LINE)) {
206                 lastLineIdx = data_size_ / BYTES_PER_LINE;
207                 if (data_size_ % BYTES_PER_LINE)
208                         lastLineIdx++;
209         }
210
211         // Fill address area background
212         painter.fillRect(QRect(posAddr_, event->rect().top(),
213                 posHex_ - (GAP_ADR_HEX / 2), height()), palette().color(QPalette::Window));
214
215         // Paint divider line between hex and ASCII areas
216         int line_x = posAscii_ - (GAP_HEX_ASCII / 2);
217         painter.setPen(palette().color(QPalette::Midlight));
218         painter.drawLine(line_x, event->rect().top(), line_x, height());
219
220         // Paint address area
221         painter.setPen(palette().color(QPalette::ButtonText));
222
223         int yStart = charHeight_;
224         for (size_t lineIdx = firstLineIdx, y = yStart; lineIdx < lastLineIdx; lineIdx++) {
225
226                 QString address = QString("%1").arg(lineIdx * 16, 10, 16, QChar('0')).toUpper();
227                 painter.drawText(posAddr_, y, address);
228                 y += charHeight_;
229         }
230
231         // Paint hex values
232         QBrush regular = palette().buttonText();
233         QBrush selected = palette().highlight();
234
235         bool multiple_chunks = (data_->chunks.size() > 1);
236         unsigned int chunk_color = 0;
237
238         initialize_byte_iterator(firstLineIdx * BYTES_PER_LINE);
239         yStart = charHeight_;
240         for (size_t lineIdx = firstLineIdx, y = yStart; lineIdx < lastLineIdx; lineIdx++) {
241
242                 int x = posHex_;
243                 for (size_t i = 0; (i < BYTES_PER_LINE) && (current_offset_ < data_size_); i++) {
244                         size_t pos = (lineIdx * BYTES_PER_LINE + i) * 2;
245
246                         // Fetch byte
247                         bool is_next_chunk;
248                         uint8_t byte_value = get_next_byte(&is_next_chunk);
249
250                         if (is_next_chunk) {
251                                 chunk_color++;
252                                 if (chunk_color == chunk_colors_.size())
253                                         chunk_color = 0;
254                         }
255
256                         if ((pos >= selectBegin_) && (pos < selectEnd_)) {
257                                 painter.setBackgroundMode(Qt::OpaqueMode);
258                                 painter.setBackground(selected);
259                                 painter.setPen(palette().color(QPalette::HighlightedText));
260                         } else {
261                                 painter.setBackground(regular);
262                                 painter.setBackgroundMode(Qt::TransparentMode);
263                                 if (!multiple_chunks)
264                                         painter.setPen(palette().color(QPalette::Text));
265                                 else
266                                         painter.setPen(chunk_colors_[chunk_color]);
267                         }
268
269                         // First nibble
270                         QString val = QString::number((byte_value & 0xF0) >> 4, 16).toUpper();
271                         painter.drawText(x, y, val);
272
273                         // Second nibble
274                         val = QString::number((byte_value & 0xF), 16).toUpper();
275                         painter.drawText(x + charWidth_, y, val);
276
277                         x += 3 * charWidth_;
278                 }
279
280                 y += charHeight_;
281         }
282
283         // Paint ASCII characters
284         initialize_byte_iterator(firstLineIdx * BYTES_PER_LINE);
285         yStart = charHeight_;
286         for (size_t lineIdx = firstLineIdx, y = yStart; lineIdx < lastLineIdx; lineIdx++) {
287
288                 int x = posAscii_;
289                 for (size_t i = 0; (i < BYTES_PER_LINE) && (current_offset_ < data_size_); i++) {
290                         // Fetch byte
291                         uint8_t ch = get_next_byte();
292
293                         if ((ch < 0x20) || (ch > 0x7E))
294                                 ch = '.';
295
296                         size_t pos = (lineIdx * BYTES_PER_LINE + i) * 2;
297                         if ((pos >= selectBegin_) && (pos < selectEnd_)) {
298                                 painter.setBackgroundMode(Qt::OpaqueMode);
299                                 painter.setBackground(selected);
300                                 painter.setPen(palette().color(QPalette::HighlightedText));
301                         } else {
302                                 painter.setBackgroundMode(Qt::TransparentMode);
303                                 painter.setBackground(regular);
304                                 painter.setPen(palette().color(QPalette::Text));
305                         }
306
307                         painter.drawText(x, y, QString(ch));
308                         x += charWidth_;
309                 }
310
311                 y += charHeight_;
312         }
313
314         // Paint cursor
315         if (hasFocus()) {
316                 int x = (cursorPos_ % (2 * BYTES_PER_LINE));
317                 int y = cursorPos_ / (2 * BYTES_PER_LINE);
318                 y -= firstLineIdx;
319                 int cursorX = (((x / 2) * 3) + (x % 2)) * charWidth_ + posHex_;
320                 int cursorY = y * charHeight_ + 4;
321                 painter.fillRect(cursorX, cursorY, 2, charHeight_, palette().color(QPalette::WindowText));
322         }
323 }
324
325 void QHexView::keyPressEvent(QKeyEvent *event)
326 {
327         bool setVisible = false;
328
329         // Cursor movements
330         if (event->matches(QKeySequence::MoveToNextChar)) {
331                 setCursorPos(cursorPos_ + 1);
332                 resetSelection(cursorPos_);
333                 setVisible = true;
334         }
335         if (event->matches(QKeySequence::MoveToPreviousChar)) {
336                 setCursorPos(cursorPos_ - 1);
337                 resetSelection(cursorPos_);
338                 setVisible = true;
339         }
340
341         if (event->matches(QKeySequence::MoveToEndOfLine)) {
342                 setCursorPos(cursorPos_ | ((BYTES_PER_LINE * 2) - 1));
343                 resetSelection(cursorPos_);
344                 setVisible = true;
345         }
346         if (event->matches(QKeySequence::MoveToStartOfLine)) {
347                 setCursorPos(cursorPos_ | (cursorPos_ % (BYTES_PER_LINE * 2)));
348                 resetSelection(cursorPos_);
349                 setVisible = true;
350         }
351         if (event->matches(QKeySequence::MoveToPreviousLine)) {
352                 setCursorPos(cursorPos_ - BYTES_PER_LINE * 2);
353                 resetSelection(cursorPos_);
354                 setVisible = true;
355         }
356         if (event->matches(QKeySequence::MoveToNextLine)) {
357                 setCursorPos(cursorPos_ + BYTES_PER_LINE * 2);
358                 resetSelection(cursorPos_);
359                 setVisible = true;
360         }
361
362         if (event->matches(QKeySequence::MoveToNextPage)) {
363                 setCursorPos(cursorPos_ + (viewport()->height() / charHeight_ - 1) * 2 * BYTES_PER_LINE);
364                 resetSelection(cursorPos_);
365                 setVisible = true;
366         }
367         if (event->matches(QKeySequence::MoveToPreviousPage)) {
368                 setCursorPos(cursorPos_ - (viewport()->height() / charHeight_ - 1) * 2 * BYTES_PER_LINE);
369                 resetSelection(cursorPos_);
370                 setVisible = true;
371         }
372         if (event->matches(QKeySequence::MoveToEndOfDocument)) {
373                 setCursorPos(data_size_ * 2);
374                 resetSelection(cursorPos_);
375                 setVisible = true;
376         }
377         if (event->matches(QKeySequence::MoveToStartOfDocument)) {
378                 setCursorPos(0);
379                 resetSelection(cursorPos_);
380                 setVisible = true;
381         }
382
383         // Select commands
384         if (event->matches(QKeySequence::SelectAll)) {
385                 resetSelection(0);
386                 setSelection(2 * data_size_);
387                 setVisible = true;
388         }
389         if (event->matches(QKeySequence::SelectNextChar)) {
390                 int pos = cursorPos_ + 1;
391                 setCursorPos(pos);
392                 setSelection(pos);
393                 setVisible = true;
394         }
395         if (event->matches(QKeySequence::SelectPreviousChar)) {
396                 int pos = cursorPos_ - 1;
397                 setSelection(pos);
398                 setCursorPos(pos);
399                 setVisible = true;
400         }
401         if (event->matches(QKeySequence::SelectEndOfLine)) {
402                 int pos = cursorPos_ - (cursorPos_ % (2 * BYTES_PER_LINE)) + (2 * BYTES_PER_LINE);
403                 setCursorPos(pos);
404                 setSelection(pos);
405                 setVisible = true;
406         }
407         if (event->matches(QKeySequence::SelectStartOfLine)) {
408                 int pos = cursorPos_ - (cursorPos_ % (2 * BYTES_PER_LINE));
409                 setCursorPos(pos);
410                 setSelection(pos);
411                 setVisible = true;
412         }
413         if (event->matches(QKeySequence::SelectPreviousLine)) {
414                 int pos = cursorPos_ - (2 * BYTES_PER_LINE);
415                 setCursorPos(pos);
416                 setSelection(pos);
417                 setVisible = true;
418         }
419         if (event->matches(QKeySequence::SelectNextLine)) {
420                 int pos = cursorPos_ + (2 * BYTES_PER_LINE);
421                 setCursorPos(pos);
422                 setSelection(pos);
423                 setVisible = true;
424         }
425
426         if (event->matches(QKeySequence::SelectNextPage)) {
427                 int pos = cursorPos_ + (((viewport()->height() / charHeight_) - 1) * 2 * BYTES_PER_LINE);
428                 setCursorPos(pos);
429                 setSelection(pos);
430                 setVisible = true;
431         }
432         if (event->matches(QKeySequence::SelectPreviousPage)) {
433                 int pos = cursorPos_ - (((viewport()->height() / charHeight_) - 1) * 2 * BYTES_PER_LINE);
434                 setCursorPos(pos);
435                 setSelection(pos);
436                 setVisible = true;
437         }
438         if (event->matches(QKeySequence::SelectEndOfDocument)) {
439                 int pos = data_size_ * 2;
440                 setCursorPos(pos);
441                 setSelection(pos);
442                 setVisible = true;
443         }
444         if (event->matches(QKeySequence::SelectStartOfDocument)) {
445                 setCursorPos(0);
446                 setSelection(0);
447                 setVisible = true;
448         }
449
450         if (event->matches(QKeySequence::Copy) && (data_)) {
451                 QString text;
452
453                 initialize_byte_iterator(selectBegin_ / 2);
454
455                 size_t selectedSize = (selectEnd_ - selectBegin_ + 1) / 2;
456                 for (size_t i = 0; i < selectedSize; i++) {
457                         uint8_t byte_value = get_next_byte();
458
459                         QString s = QString::number((byte_value & 0xF0) >> 4, 16).toUpper() +
460                                 QString::number((byte_value & 0xF), 16).toUpper() + " ";
461                         text += s;
462
463                         if (i % BYTES_PER_LINE == (BYTES_PER_LINE - 1))
464                                 text += "\n";
465                 }
466
467                 QClipboard *clipboard = QApplication::clipboard();
468                 clipboard->setText(text, QClipboard::Clipboard);
469                 if (clipboard->supportsSelection())
470                         clipboard->setText(text, QClipboard::Selection);
471         }
472
473         if (setVisible)
474                 ensureVisible();
475
476         viewport()->update();
477 }
478
479 void QHexView::mouseMoveEvent(QMouseEvent *event)
480 {
481         int actPos = cursorPosFromMousePos(event->pos());
482         setCursorPos(actPos);
483         setSelection(actPos);
484
485         viewport()->update();
486 }
487
488 void QHexView::mousePressEvent(QMouseEvent *event)
489 {
490         int cPos = cursorPosFromMousePos(event->pos());
491
492         if ((QApplication::keyboardModifiers() & Qt::ShiftModifier) && (event->button() == Qt::LeftButton))
493                 setSelection(cPos);
494         else
495                 resetSelection(cPos);
496
497         setCursorPos(cPos);
498
499         viewport()->update();
500 }
501
502 size_t QHexView::cursorPosFromMousePos(const QPoint &position)
503 {
504         size_t pos = -1;
505
506         if (((size_t)position.x() >= posHex_) &&
507                 ((size_t)position.x() < (posHex_ + HEXCHARS_IN_LINE * charWidth_))) {
508
509                 // Note: We add 1.5 character widths so that selection across
510                 // byte gaps is smoother
511                 size_t x = (position.x() + (1.5 * charWidth_ / 2) - posHex_) / charWidth_;
512
513                 // Note: We allow only full bytes to be selected, not nibbles,
514                 // so we round to the nearest byte gap
515                 x = (2 * x + 1) / 3;
516
517                 size_t firstLineIdx = verticalScrollBar()->value();
518                 size_t y = (position.y() / charHeight_) * 2 * BYTES_PER_LINE;
519                 pos = x + y + firstLineIdx * BYTES_PER_LINE * 2;
520         }
521
522         size_t max_pos = data_size_ * 2;
523
524         return std::min(pos, max_pos);
525 }
526
527 void QHexView::resetSelection()
528 {
529         selectBegin_ = selectInit_;
530         selectEnd_ = selectInit_;
531 }
532
533 void QHexView::resetSelection(int pos)
534 {
535         if (pos < 0)
536                 pos = 0;
537
538         selectInit_ = pos;
539         selectBegin_ = pos;
540         selectEnd_ = pos;
541 }
542
543 void QHexView::setSelection(int pos)
544 {
545         if (pos < 0)
546                 pos = 0;
547
548         if ((size_t)pos >= selectInit_) {
549                 selectEnd_ = pos;
550                 selectBegin_ = selectInit_;
551         } else {
552                 selectBegin_ = pos;
553                 selectEnd_ = selectInit_;
554         }
555 }
556
557 void QHexView::setCursorPos(int position)
558 {
559         if (position < 0)
560                 position = 0;
561
562         int max_pos = data_size_ * 2;
563
564         if (position > max_pos)
565                 position = max_pos;
566
567         cursorPos_ = position;
568 }
569
570 void QHexView::ensureVisible()
571 {
572         QSize areaSize = viewport()->size();
573
574         int firstLineIdx = verticalScrollBar()->value();
575         int lastLineIdx = firstLineIdx + areaSize.height() / charHeight_;
576
577         int cursorY = cursorPos_ / (2 * BYTES_PER_LINE);
578
579         if (cursorY < firstLineIdx)
580                 verticalScrollBar()->setValue(cursorY);
581         else
582                 if(cursorY >= lastLineIdx)
583                         verticalScrollBar()->setValue(cursorY - areaSize.height() / charHeight_ + 1);
584 }