3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <gerald@wireshark.org>
5 * Copyright 1998 Gerald Combs
7 * SPDX-License-Identifier: GPL-2.0-or-later
10 #include "byte_view_text.h"
12 #include <epan/charsets.h>
14 #include <wsutil/utf8_entities.h>
16 #include <ui/qt/utils/color_utils.h>
17 #include "wireshark_application.h"
18 #include "ui/recent.h"
20 #include <QActionGroup>
21 #include <QMouseEvent>
25 #include <QStyleOption>
26 #include <QTextLayout>
29 // - Add recent settings and context menu items to show/hide the offset.
30 // - Add a UTF-8 and possibly UTF-xx option to the ASCII display.
31 // - Move more common metrics to DataPrinter.
33 Q_DECLARE_METATYPE(bytes_view_type)
34 Q_DECLARE_METATYPE(bytes_encoding_type)
35 Q_DECLARE_METATYPE(DataPrinter::DumpType)
37 ByteViewText::ByteViewText(const QByteArray &data, packet_char_enc encoding, QWidget *parent) :
38 QAbstractScrollArea(parent),
39 layout_(new QTextLayout()),
42 hovered_byte_offset_(-1),
43 marked_byte_offset_(-1),
53 row_width_(recent.gui_bytes_view == BYTES_HEX ? 16 : 8),
57 layout_->setCacheEnabled(true);
59 offset_normal_fg_ = ColorUtils::alphaBlend(palette().windowText(), palette().window(), 0.35);
60 offset_field_fg_ = ColorUtils::alphaBlend(palette().windowText(), palette().window(), 0.65);
64 setMouseTracking(true);
67 setAttribute(Qt::WA_MacShowFocusRect, true);
71 ByteViewText::~ByteViewText()
77 void ByteViewText::createContextMenu()
81 QActionGroup * copy_actions = DataPrinter::copyActions(this);
82 ctx_menu_.addActions(copy_actions->actions());
83 ctx_menu_.addSeparator();
85 QActionGroup * format_actions = new QActionGroup(this);
86 action = format_actions->addAction(tr("Show bytes as hexadecimal"));
87 action->setData(QVariant::fromValue(BYTES_HEX));
88 action->setCheckable(true);
89 if (recent.gui_bytes_view == BYTES_HEX) {
90 action->setChecked(true);
92 action = format_actions->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS "as bits"));
93 action->setData(QVariant::fromValue(BYTES_BITS));
94 action->setCheckable(true);
95 if (recent.gui_bytes_view == BYTES_BITS) {
96 action->setChecked(true);
99 ctx_menu_.addActions(format_actions->actions());
100 connect(format_actions, SIGNAL(triggered(QAction*)), this, SLOT(setHexDisplayFormat(QAction*)));
102 ctx_menu_.addSeparator();
104 QActionGroup * encoding_actions = new QActionGroup(this);
105 action = encoding_actions->addAction(tr("Show text based on packet"));
106 action->setData(QVariant::fromValue(BYTES_ENC_FROM_PACKET));
107 action->setCheckable(true);
108 if (recent.gui_bytes_encoding == BYTES_ENC_FROM_PACKET) {
109 action->setChecked(true);
111 action = encoding_actions->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS "as ASCII"));
112 action->setData(QVariant::fromValue(BYTES_ENC_ASCII));
113 action->setCheckable(true);
114 if (recent.gui_bytes_encoding == BYTES_ENC_ASCII) {
115 action->setChecked(true);
117 action = encoding_actions->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS "as EBCDIC"));
118 action->setData(QVariant::fromValue(BYTES_ENC_EBCDIC));
119 action->setCheckable(true);
120 if (recent.gui_bytes_encoding == BYTES_ENC_EBCDIC) {
121 action->setChecked(true);
124 ctx_menu_.addActions(encoding_actions->actions());
125 connect(encoding_actions, SIGNAL(triggered(QAction*)), this, SLOT(setCharacterEncoding(QAction*)));
128 bool ByteViewText::isEmpty() const
130 return data_.isEmpty();
133 QSize ByteViewText::minimumSizeHint() const
135 // Allow panel to shrink to any size
139 void ByteViewText::markProtocol(int start, int length)
141 proto_start_ = start;
143 viewport()->update();
146 void ByteViewText::markField(int start, int length, bool scroll_to)
148 field_start_ = start;
150 // This might be called as a result of (de)selecting a proto tree
151 // item, so take us out of marked mode.
152 marked_byte_offset_ = -1;
156 viewport()->update();
159 void ByteViewText::markAppendix(int start, int length)
161 field_a_start_ = start;
162 field_a_len_ = length;
163 viewport()->update();
166 void ByteViewText::setMonospaceFont(const QFont &mono_font)
168 mono_font_ = QFont(mono_font);
169 mono_font_.setStyleStrategy(QFont::ForceIntegerMetrics);
171 const QFontMetricsF fm(mono_font_);
172 font_width_ = fm.width('M');
175 viewport()->setFont(mono_font_);
176 layout_->setFont(mono_font_);
178 // We should probably use ProtoTree::rowHeight.
179 line_height_ = fontMetrics().height();
182 viewport()->update();
185 void ByteViewText::paintEvent(QPaintEvent *)
187 QPainter painter(viewport());
188 painter.translate(-horizontalScrollBar()->value() * font_width_, 0);
190 // Pixel offset of this row
193 // Starting byte offset
194 int offset = verticalScrollBar()->value() * row_width_;
197 painter.fillRect(viewport()->rect(), palette().base());
199 // Offset background. We want the entire height to be filled.
201 QRect offset_rect = QRect(viewport()->rect());
202 offset_rect.setWidth(offsetPixels());
203 painter.fillRect(offset_rect, palette().window());
206 if ( data_.isEmpty() ) {
211 int widget_height = height();
212 int leading = fontMetrics().leading();
215 x_pos_to_column_.clear();
216 while( (int) (row_y + line_height_) < widget_height && offset < (int) data_.count()) {
217 drawLine(&painter, offset, row_y);
218 offset += row_width_;
219 row_y += line_height_ + leading;
224 // We can't do this in drawLine since the next line might draw over our rect.
225 if (!hover_outlines_.isEmpty()) {
226 qreal pen_width = 1.0;
227 qreal hover_alpha = 0.6;
229 QColor ho_color = palette().text().color();
230 if (marked_byte_offset_ < 0) {
232 #if QT_VERSION >= QT_VERSION_CHECK(5, 1, 0)
233 if (devicePixelRatio() > 1) {
238 ho_pen.setWidthF(pen_width);
239 ho_color.setAlphaF(hover_alpha);
240 ho_pen.setColor(ho_color);
243 painter.setPen(ho_pen);
244 painter.setBrush(Qt::NoBrush);
245 foreach (QRect ho_rect, hover_outlines_) {
246 // These look good on retina and non-retina displays on macOS.
247 // We might want to use fontMetrics numbers instead.
248 ho_rect.adjust(-1, 0, -1, -1);
249 painter.drawRect(ho_rect);
253 hover_outlines_.clear();
255 QStyleOptionFocusRect option;
256 option.initFrom(this);
257 style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this);
260 void ByteViewText::resizeEvent(QResizeEvent *)
265 void ByteViewText::mousePressEvent (QMouseEvent *event) {
266 if (isEmpty() || !event || event->button() != Qt::LeftButton) {
270 // byteSelected does the following:
271 // - Triggers selectedFieldChanged in ProtoTree, which clears the
272 // selection and selects the corresponding (or no) item.
273 // - The new tree selection triggers markField, which clobbers
274 // marked_byte_offset_.
276 const bool hover_mode = marked_byte_offset_ < 0;
277 const int byte_offset = byteOffsetAtPixel(event->pos());
278 setUpdatesEnabled(false);
279 emit byteSelected(byte_offset);
280 if (hover_mode && byte_offset >= 0) {
281 // Switch to marked mode.
282 hovered_byte_offset_ = -1;
283 marked_byte_offset_ = byte_offset;
284 viewport()->update();
286 // Back to hover mode.
287 mouseMoveEvent(event);
289 setUpdatesEnabled(true);
292 void ByteViewText::mouseMoveEvent(QMouseEvent *event)
294 if (marked_byte_offset_ >= 0) {
298 hovered_byte_offset_ = byteOffsetAtPixel(event->pos());
299 emit byteHovered(hovered_byte_offset_);
300 viewport()->update();
303 void ByteViewText::leaveEvent(QEvent *event)
305 hovered_byte_offset_ = -1;
306 emit byteHovered(hovered_byte_offset_);
308 viewport()->update();
309 QAbstractScrollArea::leaveEvent(event);
312 void ByteViewText::contextMenuEvent(QContextMenuEvent *event)
314 ctx_menu_.exec(event->globalPos());
319 const int ByteViewText::separator_interval_ = DataPrinter::separatorInterval();
321 // Draw a line of byte view text for a given offset.
322 // Text highlighting is handled using QTextLayout::FormatRange.
323 void ByteViewText::drawLine(QPainter *painter, const int offset, const int row_y)
329 // Build our pixel to byte offset vector the first time through.
330 bool build_x_pos = x_pos_to_column_.empty() ? true : false;
331 int tvb_len = data_.count();
332 int max_tvb_pos = qMin(offset + row_width_, tvb_len) - 1;
333 QList<QTextLayout::FormatRange> fmt_list;
335 static const guchar hexchars[16] = {
336 '0', '1', '2', '3', '4', '5', '6', '7',
337 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
340 HighlightMode offset_mode = ModeOffsetNormal;
344 line = QString(" %1 ").arg(offset, offsetChars(false), 16, QChar('0'));
346 x_pos_to_column_.fill(-1, fontMetrics().width(line));
352 int ascii_start = line.length() + DataPrinter::hexChars() + 3;
353 // Extra hover space before and after each byte.
354 int slop = font_width_ / 2;
357 x_pos_to_column_ += QVector<int>().fill(-1, slop);
360 for (int tvb_pos = offset; tvb_pos <= max_tvb_pos; tvb_pos++) {
362 /* insert a space every separator_interval_ bytes */
363 if ((tvb_pos != offset) && ((tvb_pos % separator_interval_) == 0)) {
365 x_pos_to_column_ += QVector<int>().fill(tvb_pos - offset - 1, font_width_);
368 switch (recent.gui_bytes_view) {
370 line += hexchars[(data_[tvb_pos] & 0xf0) >> 4];
371 line += hexchars[data_[tvb_pos] & 0x0f];
375 for (int j = 7; j >= 0; j--) {
376 line += (data_[tvb_pos] & (1 << j)) ? '1' : '0';
381 x_pos_to_column_ += QVector<int>().fill(tvb_pos - offset, fontMetrics().width(line) - x_pos_to_column_.size() + slop);
383 if (tvb_pos == hovered_byte_offset_ || tvb_pos == marked_byte_offset_) {
384 int ho_len = recent.gui_bytes_view == BYTES_HEX ? 2 : 8;
385 QRect ho_rect = painter->boundingRect(QRect(), Qt::AlignHCenter|Qt::AlignVCenter, line.right(ho_len));
386 ho_rect.moveRight(fontMetrics().width(line));
387 ho_rect.moveTop(row_y);
388 hover_outlines_.append(ho_rect);
391 line += QString(ascii_start - line.length(), ' ');
393 x_pos_to_column_ += QVector<int>().fill(-1, fontMetrics().width(line) - x_pos_to_column_.size());
396 addHexFormatRange(fmt_list, proto_start_, proto_len_, offset, max_tvb_pos, ModeProtocol);
397 if (addHexFormatRange(fmt_list, field_start_, field_len_, offset, max_tvb_pos, ModeField)) {
398 offset_mode = ModeOffsetField;
400 addHexFormatRange(fmt_list, field_a_start_, field_a_len_, offset, max_tvb_pos, ModeField);
405 bool in_non_printable = false;
410 for (int tvb_pos = offset; tvb_pos <= max_tvb_pos; tvb_pos++) {
411 /* insert a space every separator_interval_ bytes */
412 if ((tvb_pos != offset) && ((tvb_pos % separator_interval_) == 0)) {
415 x_pos_to_column_ += QVector<int>().fill(tvb_pos - offset - 1, font_width_ / 2);
419 if (recent.gui_bytes_encoding != BYTES_ENC_EBCDIC && encoding_ == PACKET_CHAR_ENC_CHAR_ASCII) {
422 c = EBCDIC_to_ASCII1(data_[tvb_pos]);
425 if (g_ascii_isprint(c)) {
427 if (in_non_printable) {
428 in_non_printable = false;
429 addAsciiFormatRange(fmt_list, np_start, np_len, offset, max_tvb_pos, ModeNonPrintable);
432 line += UTF8_MIDDLE_DOT;
433 if (!in_non_printable) {
434 in_non_printable = true;
442 x_pos_to_column_ += QVector<int>().fill(tvb_pos - offset, fontMetrics().width(line) - x_pos_to_column_.size());
444 if (tvb_pos == hovered_byte_offset_ || tvb_pos == marked_byte_offset_) {
445 QRect ho_rect = painter->boundingRect(QRect(), 0, line.right(1));
446 ho_rect.moveRight(fontMetrics().width(line));
447 ho_rect.moveTop(row_y);
448 hover_outlines_.append(ho_rect);
451 if (in_non_printable) {
452 addAsciiFormatRange(fmt_list, np_start, np_len, offset, max_tvb_pos, ModeNonPrintable);
454 addAsciiFormatRange(fmt_list, proto_start_, proto_len_, offset, max_tvb_pos, ModeProtocol);
455 if (addAsciiFormatRange(fmt_list, field_start_, field_len_, offset, max_tvb_pos, ModeField)) {
456 offset_mode = ModeOffsetField;
458 addAsciiFormatRange(fmt_list, field_a_start_, field_a_len_, offset, max_tvb_pos, ModeField);
461 // XXX Fields won't be highlighted if neither hex nor ascii are enabled.
462 addFormatRange(fmt_list, 0, offsetChars(), offset_mode);
464 layout_->clearLayout();
465 layout_->clearAdditionalFormats();
466 layout_->setText(line);
467 layout_->setAdditionalFormats(fmt_list);
468 layout_->beginLayout();
469 QTextLine tl = layout_->createLine();
470 tl.setLineWidth(totalPixels());
471 tl.setLeadingIncluded(true);
472 layout_->endLayout();
473 layout_->draw(painter, QPointF(0.0, row_y));
476 bool ByteViewText::addFormatRange(QList<QTextLayout::FormatRange> &fmt_list, int start, int length, HighlightMode mode)
481 QTextLayout::FormatRange format_range;
482 format_range.start = start;
483 format_range.length = length;
488 format_range.format.setBackground(palette().highlight());
491 format_range.format.setBackground(palette().window());
493 case ModeOffsetNormal:
494 format_range.format.setForeground(offset_normal_fg_);
496 case ModeOffsetField:
497 format_range.format.setForeground(offset_field_fg_);
499 case ModeNonPrintable:
500 format_range.format.setForeground(offset_normal_fg_);
503 fmt_list << format_range;
507 bool ByteViewText::addHexFormatRange(QList<QTextLayout::FormatRange> &fmt_list, int mark_start, int mark_length, int tvb_offset, int max_tvb_pos, ByteViewText::HighlightMode mode)
509 int mark_end = mark_start + mark_length - 1;
510 if (mark_start < 0 || mark_length < 1) return false;
511 if (mark_start > max_tvb_pos && mark_end < tvb_offset) return false;
513 int chars_per_byte = recent.gui_bytes_view == BYTES_HEX ? 2 : 8;
514 int chars_plus_pad = chars_per_byte + 1;
515 int byte_start = qMax(tvb_offset, mark_start) - tvb_offset;
516 int byte_end = qMin(max_tvb_pos, mark_end) - tvb_offset;
517 int fmt_start = offsetChars() + 1 // offset + spacing
518 + (byte_start / separator_interval_)
519 + (byte_start * chars_plus_pad);
520 int fmt_length = offsetChars() + 1 // offset + spacing
521 + (byte_end / separator_interval_)
522 + (byte_end * chars_plus_pad)
525 return addFormatRange(fmt_list, fmt_start, fmt_length, mode);
528 bool ByteViewText::addAsciiFormatRange(QList<QTextLayout::FormatRange> &fmt_list, int mark_start, int mark_length, int tvb_offset, int max_tvb_pos, ByteViewText::HighlightMode mode)
530 int mark_end = mark_start + mark_length - 1;
531 if (mark_start < 0 || mark_length < 1) return false;
532 if (mark_start > max_tvb_pos && mark_end < tvb_offset) return false;
534 int byte_start = qMax(tvb_offset, mark_start) - tvb_offset;
535 int byte_end = qMin(max_tvb_pos, mark_end) - tvb_offset;
536 int fmt_start = offsetChars() + DataPrinter::hexChars() + 3 // offset + hex + spacing
537 + (byte_start / separator_interval_)
539 int fmt_length = offsetChars() + DataPrinter::hexChars() + 3 // offset + hex + spacing
540 + (byte_end / separator_interval_)
542 + 1 // Just one character.
544 return addFormatRange(fmt_list, fmt_start, fmt_length, mode);
547 void ByteViewText::scrollToByte(int byte)
549 verticalScrollBar()->setValue(byte / row_width_);
552 // Offset character width
553 int ByteViewText::offsetChars(bool include_pad)
555 int padding = include_pad ? 2 : 0;
556 if (! isEmpty() && data_.count() > 0xffff) {
562 // Offset pixel width
563 int ByteViewText::offsetPixels()
566 // One pad space before and after
567 QString zeroes = QString(offsetChars(), '0');
568 return fontMetrics().width(zeroes);
574 int ByteViewText::hexPixels()
577 // One pad space before and after
578 QString zeroes = QString(DataPrinter::hexChars() + 2, '0');
579 return fontMetrics().width(zeroes);
584 int ByteViewText::asciiPixels()
587 // Two pad spaces before, one after
588 int ascii_chars = (row_width_ + ((row_width_ - 1) / separator_interval_));
589 QString zeroes = QString(ascii_chars + 3, '0');
590 return fontMetrics().width(zeroes);
595 int ByteViewText::totalPixels()
597 return offsetPixels() + hexPixels() + asciiPixels();
600 void ByteViewText::copyBytes(bool)
602 QAction* action = qobject_cast<QAction*>(sender());
607 int dump_type = action->data().toInt();
609 if (dump_type <= DataPrinter::DP_Binary) {
611 printer.toClipboard((DataPrinter::DumpType) dump_type, this);
615 // We do chunky (per-character) scrolling because it makes some of the
616 // math easier. Should we do smooth scrolling?
617 void ByteViewText::updateScrollbars()
619 const int length = data_.count();
621 int all_lines_height = length / row_width_ + ((length % row_width_) ? 1 : 0) - viewport()->height() / line_height_;
623 verticalScrollBar()->setRange(0, qMax(0, all_lines_height));
624 horizontalScrollBar()->setRange(0, qMax(0, int((totalPixels() - viewport()->width()) / font_width_)));
628 int ByteViewText::byteOffsetAtPixel(QPoint pos)
630 int byte = (verticalScrollBar()->value() + (pos.y() / line_height_)) * row_width_;
631 int x = (horizontalScrollBar()->value() * font_width_) + pos.x();
632 int col = x_pos_to_column_.value(x, -1);
639 if (byte > data_.count()) {
645 void ByteViewText::setHexDisplayFormat(QAction *action)
651 recent.gui_bytes_view = action->data().value<bytes_view_type>();
652 row_width_ = recent.gui_bytes_view == BYTES_HEX ? 16 : 8;
654 viewport()->update();
657 void ByteViewText::setCharacterEncoding(QAction *action)
663 recent.gui_bytes_encoding = action->data().value<bytes_encoding_type>();
664 viewport()->update();
673 * indent-tabs-mode: nil
676 * ex: set shiftwidth=4 tabstop=8 expandtab:
677 * :indentSize=4:tabSize=8:noTabs=true: