1 /* packet_diagram.cpp
2  *
3  * Wireshark - Network traffic analyzer
4  * By Gerald Combs <gerald@wireshark.org>
5  * Copyright 1998 Gerald Combs
6  *
7  * SPDX-License-Identifier: GPL-2.0-or-later
8  */
9 
10 #include "packet_diagram.h"
11 
12 #include "math.h"
13 
14 #include "epan/epan.h"
15 #include "epan/epan_dissect.h"
16 
17 #include "wsutil/utf8_entities.h"
18 
19 #include "wireshark_application.h"
20 
21 #include "ui/qt/main_window.h"
22 #include "ui/qt/utils/proto_node.h"
23 #include "ui/qt/utils/variant_pointer.h"
24 #include "ui/recent.h"
25 
26 
27 #include <QContextMenuEvent>
28 #include <QGraphicsItem>
29 #include <QMenu>
30 
31 #if defined(QT_SVG_LIB) && 0
32 #include <QBuffer>
33 #include <QMimeData>
34 #include <QSvgGenerator>
35 #endif
36 
37 // Item offsets and lengths
38 //#define DEBUG_PACKET_DIAGRAM 1
39 
40 #ifdef DEBUG_PACKET_DIAGRAM
41 #include <QDebug>
42 #endif
43 
44 // "rems" are root em widths, aka the regular font height, similar to rems in CSS.
45 class DiagramLayout {
46 public:
DiagramLayout()47     DiagramLayout() :
48         bits_per_row_(32),
49         small_font_rems_(0.75),
50         bit_width_rems_(1.0),
51         padding_rems_(0.5),
52         span_mark_offset_rems_(0.2)
53     {
54         setFont(wsApp->font());
55     }
56 
setFont(QFont font)57     void setFont(QFont font) {
58         regular_font_ = font;
59         small_font_ = font;
60         small_font_.setPointSize(regular_font_.pointSize() * small_font_rems_);
61 
62         QFontMetrics fm(regular_font_);
63         root_em_ = fm.height();
64     }
setShowFields(bool show_fields=false)65     void setShowFields(bool show_fields = false) { recent.gui_packet_diagram_field_values = show_fields; }
66 
bitsPerRow() const67     int bitsPerRow() const { return bits_per_row_; }
regularFont() const68     const QFont regularFont() const { return regular_font_; }
smallFont() const69     const QFont smallFont() const { return small_font_; }
bitWidth() const70     int bitWidth() const { return root_em_ * bit_width_rems_; }
lineHeight() const71     int lineHeight() const { return root_em_; }
hPadding() const72     int hPadding() const { return root_em_ * padding_rems_; }
vPadding() const73     int vPadding() const { return root_em_ * padding_rems_; }
spanMarkOffset() const74     int spanMarkOffset() const { return root_em_ * span_mark_offset_rems_; }
rowHeight() const75     int rowHeight() const {
76         int rows = recent.gui_packet_diagram_field_values ? 2 : 1;
77         return ((lineHeight() * rows) + (vPadding() * 2));
78     }
showFields() const79     bool showFields() const { return recent.gui_packet_diagram_field_values; }
80 private:
81     int bits_per_row_;
82     double small_font_rems_;
83     double bit_width_rems_;
84     double padding_rems_;
85     double span_mark_offset_rems_; // XXX Make this padding_rems_ / 2 instead?
86     QFont regular_font_;
87     QFont small_font_;
88     int root_em_;
89 };
90 
91 class FieldInformationGraphicsItem : public QGraphicsPolygonItem
92 {
93 public:
FieldInformationGraphicsItem(field_info * fi,int start_bit,int fi_length,const DiagramLayout * layout,QGraphicsItem * parent=nullptr)94     FieldInformationGraphicsItem(field_info *fi, int start_bit, int fi_length, const DiagramLayout *layout, QGraphicsItem *parent = nullptr) :
95         QGraphicsPolygonItem(QPolygonF(), parent),
96         finfo_(new FieldInformation(fi)),
97         representation_("Unknown"),
98         start_bit_(start_bit),
99         layout_(layout),
100         collapsed_len_(fi_length),
101         collapsed_row_(-1)
102     {
103         Q_ASSERT(layout_);
104 
105         for (int idx = 0; idx < NumSpanMarks; idx++) {
106             span_marks_[idx] = new QGraphicsLineItem(this);
107             span_marks_[idx]->hide();
108         }
109 
110         int bits_per_row = layout_->bitsPerRow();
111         int row1_start = start_bit_ % bits_per_row;
112         int bits_remain = fi_length;
113 
114         int row1_bits = bits_remain;
115         if (bits_remain + row1_start > bits_per_row) {
116             row1_bits = bits_per_row - row1_start;
117             bits_remain -= row1_bits;
118             if (row1_start == 0 && bits_remain >= bits_per_row) {
119                 // Collapse first row
120                 bits_remain %= bits_per_row;
121                 collapsed_row_ = 0;
122             }
123         } else {
124             bits_remain = 0;
125         }
126 
127         int row2_bits = bits_remain;
128         if (bits_remain > bits_per_row) {
129             row2_bits = bits_per_row;
130             bits_remain -= bits_per_row;
131             if (bits_remain > bits_per_row) {
132                 // Collapse second row
133                 bits_remain %= bits_per_row;
134                 collapsed_row_ = 1;
135             }
136         } else {
137             bits_remain = 0;
138         }
139         int row3_bits = bits_remain;
140 
141         collapsed_len_ = row1_bits + row2_bits + row3_bits;
142 
143         QRectF rr1, rr2, rr3;
144         QRectF row_rect = QRectF(row1_start, 0, row1_bits, 1);
145         unit_shape_ = QPolygonF(row_rect);
146         rr1 = row_rect;
147         unit_tr_ = row_rect;
148 
149         if (row2_bits > 0) {
150             row_rect = QRectF(0, 1, row2_bits, 1);
151             unit_shape_ = unit_shape_.united(QPolygonF(row_rect));
152             rr2 = row_rect;
153             if (row2_bits > row1_bits) {
154                 unit_tr_ = row_rect;
155             }
156 
157             if (row3_bits > 0) {
158                 row_rect = QRectF(0, 2, row3_bits, 1);
159                 unit_shape_ = unit_shape_.united(QPolygonF(row_rect));
160                 rr3 = row_rect;
161             }
162             QPainterPath pp;
163             pp.addPolygon(unit_shape_);
164             unit_shape_ = pp.simplified().toFillPolygon();
165         }
166 
167         updateLayout();
168 
169         if (finfo_->isValid()) {
170             setToolTip(QString("%1 (%2) = %3")
171                        .arg(finfo_->headerInfo().name)
172                        .arg(finfo_->headerInfo().abbreviation)
173                        .arg(finfo_->toString()));
174             setData(Qt::UserRole, VariantPointer<field_info>::asQVariant(finfo_->fieldInfo()));
175             representation_ = fi->rep->representation;
176         } else {
177             setToolTip(QObject::tr("Gap in dissection"));
178         }
179     }
180 
~FieldInformationGraphicsItem()181     ~FieldInformationGraphicsItem()
182     {
183         delete finfo_;
184     }
185 
collapsedLength()186     int collapsedLength() { return collapsed_len_; }
187 
setPos(qreal x,qreal y)188     void setPos(qreal x, qreal y) {
189         QGraphicsPolygonItem::setPos(x, y);
190         updateLayout();
191     }
192 
maxLeftY()193     int maxLeftY() {
194         qreal rel_len = (start_bit_ % layout_->bitsPerRow()) + collapsed_len_;
195         QPointF pt = mapToParent(QPointF(0, ceil(rel_len / layout_->bitsPerRow()) * layout_->rowHeight()));
196         return pt.y();
197     }
198 
maxRightY()199     int maxRightY() {
200         qreal rel_len = (start_bit_ % layout_->bitsPerRow()) + collapsed_len_;
201         QPointF pt = mapToParent(QPointF(0, floor(rel_len / layout_->bitsPerRow()) * layout_->rowHeight()));
202         return pt.y();
203     }
204 
paint(QPainter * painter,const QStyleOptionGraphicsItem * option,QWidget *)205     void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) {
206 
207         painter->setPen(Qt::NoPen);
208         painter->save();
209         if (!finfo_->isValid()) {
210             QBrush brush = QBrush(option->palette.text().color(), Qt::BDiagPattern);
211             painter->setBrush(brush);
212         } else if (isSelected()) {
213             painter->setBrush(option->palette.highlight().color());
214         }
215         painter->drawPolygon(polygon());
216         painter->restore();
217 
218         // Lower and inner right borders
219         painter->setPen(option->palette.text().color());
220         QPolygonF shape = polygon();
221         for (int idx = 1; idx < unit_shape_.size(); idx++) {
222             QPointF u_start = unit_shape_[idx - 1];
223             QPointF u_end = unit_shape_[idx];
224             QPointF start, end;
225             bool draw_line = false;
226 
227             if (u_start.y() > 0 && u_start.y() == u_end.y()) {
228                 draw_line = true;
229             } else if (u_start.x() > 0 && u_start.x() < layout_->bitsPerRow() && u_start.x() == u_end.x()) {
230                 draw_line = true;
231             }
232             if (draw_line) {
233                 start = shape[idx - 1];
234                 end = shape[idx];
235                 painter->drawLine(start, end);
236             }
237         }
238 
239         if (!finfo_->isValid()) {
240             return;
241         }
242 
243         // Field label(s)
244         QString label;
245         if (finfo_->headerInfo().type == FT_NONE) {
246             label = representation_;
247         } else {
248             label = finfo_->headerInfo().name;
249         }
250         paintLabel(painter, label, scaled_tr_);
251 
252         if (layout_->showFields()) {
253             label = finfo_->toString();
254             paintLabel(painter, label, scaled_tr_.adjusted(0, scaled_tr_.height(), 0, scaled_tr_.height()));
255         }
256     }
257 
258 private:
259     enum SpanMark {
260         TopLeft,
261         BottomLeft,
262         TopRight,
263         BottomRight,
264         NumSpanMarks
265     };
266     FieldInformation *finfo_;
267     QString representation_;
268     int start_bit_;
269     const DiagramLayout *layout_;
270     int collapsed_len_;
271     int collapsed_row_;
272     QPolygonF unit_shape_;
273     QRectF unit_tr_;
274     QRectF scaled_tr_;
275     QGraphicsLineItem *span_marks_[NumSpanMarks];
276 
updateLayout()277     void updateLayout() {
278         QTransform xform;
279 
280         xform.scale(layout_->bitWidth(), layout_->rowHeight());
281         setPolygon(xform.map(unit_shape_));
282         scaled_tr_ = xform.mapRect(unit_tr_);
283         scaled_tr_.adjust(layout_->hPadding(), layout_->vPadding(), -layout_->hPadding(), -layout_->vPadding());
284         scaled_tr_.setHeight(layout_->lineHeight());
285 
286         // Collapsed / span marks
287         for (int idx = 0; idx < NumSpanMarks; idx++) {
288             span_marks_[idx]->hide();
289         }
290         if (collapsed_row_ >= 0) {
291             QRectF bounding_rect = polygon().boundingRect();
292             qreal center_y = bounding_rect.top() + (layout_->rowHeight() * collapsed_row_) + (layout_->rowHeight() / 2);
293             qreal mark_w = layout_->bitWidth() / 3; // Each mark side to center
294             QLineF span_l = QLineF(-mark_w, mark_w / 2, mark_w, -mark_w / 2);
295             for (int idx = 0; idx < NumSpanMarks; idx++) {
296                 QPointF center;
297                 switch (idx) {
298                 case TopLeft:
299                     center = QPointF(bounding_rect.left(), center_y - layout_->spanMarkOffset());
300                     break;
301                 case BottomLeft:
302                     center = QPointF(bounding_rect.left(), center_y + layout_->spanMarkOffset());
303                     break;
304                 case TopRight:
305                     center = QPointF(bounding_rect.right(), center_y - layout_->spanMarkOffset());
306                     break;
307                 case BottomRight:
308                     center = QPointF(bounding_rect.right(), center_y + layout_->spanMarkOffset());
309                     break;
310                 }
311 
312                 span_marks_[idx]->setLine(span_l.translated(center));
313                 span_marks_[idx]->setZValue(zValue() - 0.1);
314                 span_marks_[idx]->show();
315             }
316         }
317     }
318 
paintLabel(QPainter * painter,QString label,QRectF label_rect)319     void paintLabel(QPainter *painter, QString label, QRectF label_rect) {
320         QFontMetrics fm = QFontMetrics(layout_->regularFont());
321 
322         painter->setFont(layout_->regularFont());
323 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
324         int label_w = fm.horizontalAdvance(label);
325 #else
326         int label_w = fm.width(label);
327 #endif
328         if (label_w > label_rect.width()) {
329             painter->setFont(layout_->smallFont());
330             fm = QFontMetrics(layout_->smallFont());
331 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
332             label_w = fm.horizontalAdvance(label);
333 #else
334             label_w = fm.width(label);
335 #endif
336             if (label_w > label_rect.width()) {
337                 // XXX Use parent+ItemClipsChildrenToShape or setScale instead?
338                 label = fm.elidedText(label, Qt::ElideRight, label_rect.width());
339             }
340         }
341         painter->drawText(label_rect, Qt::AlignCenter, label);
342     }
343 };
344 
PacketDiagram(QWidget * parent)345 PacketDiagram::PacketDiagram(QWidget *parent) :
346     QGraphicsView(parent),
347     layout_(new DiagramLayout),
348     cap_file_(nullptr),
349     root_node_(nullptr),
350     selected_field_(nullptr),
351     y_pos_(0)
352 {
353     setAccessibleName(tr("Packet diagram"));
354 
355     setRenderHint(QPainter::Antialiasing);
356 
357     // XXX Move to setMonospaceFont similar to ProtoTree
358     layout_->setFont(font());
359 
360     connect(wsApp, &WiresharkApplication::appInitialized, this, &PacketDiagram::connectToMainWindow);
361     connect(wsApp, &WiresharkApplication::zoomRegularFont, this, &PacketDiagram::setFont);
362 
363     resetScene();
364 }
365 
~PacketDiagram()366 PacketDiagram::~PacketDiagram()
367 {
368     delete layout_;
369 }
370 
setRootNode(proto_node * root_node)371 void PacketDiagram::setRootNode(proto_node *root_node)
372 {
373     // As https://doc.qt.io/qt-5/qgraphicsscene.html#clear says, this
374     // "Removes and deletes all items from the scene, but otherwise leaves
375     // the state of the scene unchanged."
376     // This means that the scene rect grows but doesn't shrink, which is
377     // useful in our case because it gives us a cheap way to retain our
378     // scroll position between packets.
379     scene()->clear();
380     selected_field_ = nullptr;
381     y_pos_ = 0;
382 
383     root_node_ = root_node;
384     if (!isVisible() || !root_node) {
385         return;
386     }
387 
388     ProtoNode parent_node(root_node_);
389     if (!parent_node.isValid()) {
390         return;
391     }
392 
393     ProtoNode::ChildIterator kids = parent_node.children();
394     while (kids.element().isValid())
395     {
396         proto_node *tl_node = kids.element().protoNode();
397         kids.next();
398 
399         // Exclude all ("Frame") and nothing
400         if (tl_node->finfo->start == 0 && tl_node->finfo->length == (int) tvb_captured_length(cap_file_->edt->tvb)) {
401             continue;
402         }
403         if (tl_node->finfo->length < 1) {
404             continue;
405         }
406         addDiagram(tl_node);
407     }
408 }
409 
clear()410 void PacketDiagram::clear()
411 {
412     setRootNode(nullptr);
413 }
414 
setCaptureFile(capture_file * cf)415 void PacketDiagram::setCaptureFile(capture_file *cf)
416 {
417     // For use by the main view, set the capture file which will later have a
418     // dissection (EDT) ready.
419     // The packet dialog sets a fixed EDT context and MUST NOT use this.
420     cap_file_ = cf;
421 
422     if (!cf) {
423         resetScene();
424     }
425 }
426 
setFont(const QFont & font)427 void PacketDiagram::setFont(const QFont &font)
428 {
429     layout_->setFont(font);
430     resetScene(false);
431 }
432 
selectedFieldChanged(FieldInformation * finfo)433 void PacketDiagram::selectedFieldChanged(FieldInformation *finfo)
434 {
435     setSelectedField(finfo ? finfo->fieldInfo() : nullptr);
436 }
437 
selectedFrameChanged(QList<int> frames)438 void PacketDiagram::selectedFrameChanged(QList<int> frames)
439 {
440     if (frames.count() == 1 && cap_file_ && cap_file_->edt && cap_file_->edt->tree) {
441         setRootNode(cap_file_->edt->tree);
442     } else {
443         // Clear the proto tree contents as they have become invalid.
444         setRootNode(nullptr);
445     }
446 }
447 
event(QEvent * event)448 bool PacketDiagram::event(QEvent *event)
449 {
450     switch (event->type()) {
451     case QEvent::ApplicationPaletteChange:
452         resetScene(false);
453         break;
454     default:
455         break;
456 
457     }
458     return QGraphicsView::event(event);
459 }
460 
contextMenuEvent(QContextMenuEvent * event)461 void PacketDiagram::contextMenuEvent(QContextMenuEvent *event)
462 {
463     if (!event) {
464         return;
465     }
466 
467     QAction *action;
468     QMenu ctx_menu(this);
469 
470     action = ctx_menu.addAction(tr("Show Field Values"));
471     action->setCheckable(true);
472     action->setChecked(layout_->showFields());
473     connect(action, &QAction::toggled, this, &PacketDiagram::showFieldsToggled);
474 
475     ctx_menu.addSeparator();
476 
477     action = ctx_menu.addAction(tr("Save Diagram As…"));
478     connect(action, &QAction::triggered, this, &PacketDiagram::saveAsTriggered);
479 
480     action = ctx_menu.addAction(tr("Copy as Raster Image"));
481     connect(action, &QAction::triggered, this, &PacketDiagram::copyAsRasterTriggered);
482 
483 #if defined(QT_SVG_LIB) && !defined(Q_OS_MAC)
484     action = ctx_menu.addAction(tr("…as SVG"));
485     connect(action, &QAction::triggered, this, &PacketDiagram::copyAsSvgTriggered);
486 #endif
487 
488     ctx_menu.exec(event->globalPos());
489 }
490 
connectToMainWindow()491 void PacketDiagram::connectToMainWindow()
492 {
493     MainWindow *main_window = qobject_cast<MainWindow *>(wsApp->mainWindow());
494     if (!main_window) {
495         return;
496     }
497     connect(main_window, &MainWindow::setCaptureFile, this, &PacketDiagram::setCaptureFile);
498     connect(main_window, &MainWindow::fieldSelected, this, &PacketDiagram::selectedFieldChanged);
499     connect(main_window, &MainWindow::framesSelected, this, &PacketDiagram::selectedFrameChanged);
500 
501     connect(this, &PacketDiagram::fieldSelected, main_window, &MainWindow::fieldSelected);
502 }
503 
sceneSelectionChanged()504 void PacketDiagram::sceneSelectionChanged()
505 {
506     field_info *sel_fi = nullptr;
507     if (! scene()->selectedItems().isEmpty()) {
508         sel_fi = VariantPointer<field_info>::asPtr(scene()->selectedItems().first()->data(Qt::UserRole));
509     }
510 
511     if (sel_fi) {
512         FieldInformation finfo(sel_fi, this);
513         emit fieldSelected(&finfo);
514     } else {
515         emit fieldSelected(nullptr);
516     }
517 }
518 
resetScene(bool reset_root)519 void PacketDiagram::resetScene(bool reset_root)
520 {
521     // As noted in setRootNode, scene()->clear() doesn't clear everything.
522     // Do a "hard" clear, which resets our various rects and scroll position.
523     if (scene()) {
524         delete scene();
525     }
526     QGraphicsScene *new_scene = new QGraphicsScene();
527     setScene(new_scene);
528     connect(new_scene, &QGraphicsScene::selectionChanged, this, &PacketDiagram::sceneSelectionChanged);
529     setRootNode(reset_root ? nullptr : root_node_);
530 }
531 
532 struct DiagramItemSpan {
533     field_info *finfo;
534     int start_bit;
535     int length;
536 };
537 
addDiagram(proto_node * tl_node)538 void PacketDiagram::addDiagram(proto_node *tl_node)
539 {
540     QGraphicsItem *item;
541     QGraphicsSimpleTextItem *t_item;
542     int bits_per_row = layout_->bitsPerRow();
543     int bit_width = layout_->bitWidth();
544     int diag_w = bit_width * layout_->bitsPerRow();
545     qreal x = layout_->hPadding();
546 
547     // Title
548     t_item = scene()->addSimpleText(tl_node->finfo->hfinfo->name);
549     t_item->setFont(layout_->regularFont());
550     t_item->setPos(0, y_pos_);
551     y_pos_ += layout_->lineHeight() + (bit_width / 4);
552 
553     int border_top = y_pos_;
554 
555     // Bit scale + tick marks
556     QList<int> tick_nums;
557     for (int tn = 0 ; tn < layout_->bitsPerRow(); tn += 16) {
558         tick_nums << tn << tn + 15;
559     }
560     qreal y_bottom = y_pos_ + bit_width;
561     QGraphicsItem *tl_item = scene()->addLine(x, y_bottom, x + diag_w, y_bottom);
562     QFontMetrics sfm = QFontMetrics(layout_->smallFont());
563 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
564     int space_w = sfm.horizontalAdvance(' ');
565 #else
566     int space_w = sfm.width(' ');
567 #endif
568 #ifdef Q_OS_WIN
569     // t_item->boundingRect() has a pixel of space on the left on my (gcc)
570     // Windows VM.
571     int tl_adjust = 1;
572 #else
573     int tl_adjust = 0;
574 #endif
575 
576     for (int tick_n = 0; tick_n < bits_per_row; tick_n++) {
577         x = layout_->hPadding() + (tick_n * bit_width);
578         qreal y_top = y_pos_ + (tick_n % 8 == 0 ? 0 : bit_width / 2);
579         if (tick_n > 0) {
580             scene()->addLine(x, y_top, x, y_bottom);
581         }
582 
583         if (tick_nums.contains(tick_n)) {
584             t_item = scene()->addSimpleText(QString::number(tick_n));
585             t_item->setFont(layout_->smallFont());
586             if (tick_n % 2 == 0) {
587                 t_item->setPos(x + space_w - tl_adjust, y_pos_);
588             } else {
589                 t_item->setPos(x + bit_width - space_w - t_item->boundingRect().width() - tl_adjust, y_pos_);
590             }
591             // Does the placement above look funny on your system? Try
592             // uncommenting the lines below.
593             // QGraphicsRectItem *br_item = scene()->addRect(t_item->boundingRect(), QPen(palette().highlight().color()));
594             // br_item->setPos(t_item->pos());
595         }
596     }
597     y_pos_ = y_bottom;
598     x = layout_->hPadding();
599 
600     // Collect our top-level fields
601     int last_start_bit = -1;
602     int max_l_y = y_bottom;
603     QList<DiagramItemSpan>item_spans;
604     for (proto_item *cur_item = tl_node->first_child; cur_item; cur_item = cur_item->next) {
605         if (proto_item_is_generated(cur_item) || proto_item_is_hidden(cur_item)) {
606             continue;
607         }
608 
609         field_info *fi = cur_item->finfo;
610         int start_bit = ((fi->start - tl_node->finfo->start) * 8) + FI_GET_BITS_OFFSET(fi);
611         int length = FI_GET_BITS_SIZE(fi) ? FI_GET_BITS_SIZE(fi) : fi->length * 8;
612 
613         if (start_bit <= last_start_bit || length <= 0) {
614 #ifdef DEBUG_PACKET_DIAGRAM
615             qDebug() << "Skipping item" << fi->hfinfo->abbrev << start_bit << last_start_bit << length;
616 #endif
617             continue;
618         }
619         last_start_bit = start_bit;
620 
621         if (item_spans.size() > 0) {
622             DiagramItemSpan prev_span = item_spans.last();
623             // Get rid of overlaps.
624             if (prev_span.start_bit + prev_span.length > start_bit) {
625 #ifdef DEBUG_PACKET_DIAGRAM
626                 qDebug() << "Resized prev" << prev_span.finfo->hfinfo->abbrev << prev_span.start_bit << prev_span.length << "->" << start_bit - prev_span.start_bit;
627 #endif
628                 prev_span.length = start_bit - prev_span.start_bit;
629             }
630             if (prev_span.length < 1) {
631 #ifdef DEBUG_PACKET_DIAGRAM
632                 qDebug() << "Removed prev" << prev_span.finfo->hfinfo->abbrev << prev_span.start_bit << prev_span.length;
633                 item_spans.removeLast();
634                 if (item_spans.size() < 1) {
635                     continue;
636                 }
637                 prev_span = item_spans.last();
638 #endif
639             }
640             // Fill in gaps.
641             if (prev_span.start_bit + prev_span.length < start_bit) {
642 #ifdef DEBUG_PACKET_DIAGRAM
643                 qDebug() << "Adding gap" << prev_span.finfo->hfinfo->abbrev << prev_span.start_bit << prev_span.length << start_bit;
644 #endif
645                 int gap_start = prev_span.start_bit + prev_span.length;
646                 DiagramItemSpan gap_span = { nullptr, gap_start, start_bit - gap_start };
647                 item_spans << gap_span;
648             }
649         }
650 
651         DiagramItemSpan item_span = { cur_item->finfo, start_bit, length };
652         item_spans << item_span;
653     }
654 
655     qreal z_value = tl_item->zValue();
656     int start_bit = 0;
657     for (int idx = 0; idx < item_spans.size(); idx++) {
658         DiagramItemSpan *item_span = &item_spans[idx];
659 
660         int y_off = (start_bit / bits_per_row) * layout_->rowHeight();
661         // Stack each item behind the previous one.
662         z_value -= .01;
663         FieldInformationGraphicsItem *fi_item = new FieldInformationGraphicsItem(item_span->finfo, start_bit, item_span->length, layout_);
664         start_bit += fi_item->collapsedLength();
665         fi_item->setPos(x, y_bottom + y_off);
666         fi_item->setFlag(QGraphicsItem::ItemIsSelectable);
667         fi_item->setAcceptedMouseButtons(Qt::LeftButton);
668         fi_item->setZValue(z_value);
669         scene()->addItem(fi_item);
670 
671         y_pos_ = fi_item->maxRightY();
672         max_l_y = fi_item->maxLeftY();
673     }
674 
675     // Left & right borders
676     scene()->addLine(x, border_top, x, max_l_y);
677     scene()->addLine(x + diag_w, border_top, x + diag_w, y_pos_);
678 
679     // Inter-diagram margin
680     y_pos_ = max_l_y + bit_width;
681 
682     // Set the proper color. Needed for dark mode on macOS + Qt 5.15.0 at least, possibly other cases.
683     foreach (item, scene()->items()) {
684         QGraphicsSimpleTextItem *t_item = qgraphicsitem_cast<QGraphicsSimpleTextItem *>(item);
685         if (t_item) {
686             t_item->setBrush(palette().text().color());
687         }
688         QGraphicsLineItem *l_item = qgraphicsitem_cast<QGraphicsLineItem *>(item);
689         if (l_item) {
690             l_item->setPen(palette().text().color());
691         }
692     }
693 }
694 
setSelectedField(field_info * fi)695 void PacketDiagram::setSelectedField(field_info *fi)
696 {
697     QSignalBlocker blocker(this);
698     FieldInformationGraphicsItem *fi_item;
699 
700     foreach (QGraphicsItem *item, scene()->items()) {
701         if (item->isSelected()) {
702             item->setSelected(false);
703         }
704         if (fi && VariantPointer<field_info>::asPtr(item->data(Qt::UserRole)) == fi) {
705             fi_item = qgraphicsitem_cast<FieldInformationGraphicsItem *>(item);
706             if (fi_item) {
707                 fi_item->setSelected(true);
708             }
709         }
710     }
711 }
712 
exportToImage()713 QImage PacketDiagram::exportToImage()
714 {
715     // Create a hi-res 2x scaled image.
716     int scale = 2;
717     QRect rr = QRect(0, 0, sceneRect().size().width() * scale, sceneRect().size().height() * scale);
718     QImage raster_diagram = QImage(rr.size(), QImage::Format_ARGB32);
719     QPainter raster_painter(&raster_diagram);
720 
721     raster_painter.setRenderHint(QPainter::Antialiasing);
722     raster_painter.fillRect(rr, palette().base().color());
723     scene()->render(&raster_painter);
724 
725     raster_painter.end();
726 
727     return raster_diagram;
728 }
729 
730 #if defined(QT_SVG_LIB) && 0
exportToSvg()731 QByteArray PacketDiagram::exportToSvg()
732 {
733     QRect sr = QRect(0, 0, sceneRect().size().width(), sceneRect().size().height());
734     QBuffer svg_buf;
735     QSvgGenerator svg_diagram;
736     svg_diagram.setSize(sr.size());
737     svg_diagram.setViewBox(sr);
738     svg_diagram.setOutputDevice(&svg_buf);
739 
740     QPainter svg_painter(&svg_diagram);
741     svg_painter.fillRect(sr, palette().base().color());
742     scene()->render(&svg_painter);
743 
744     svg_painter.end();
745 
746     return svg_buf.buffer();
747 }
748 #endif
749 
showFieldsToggled(bool checked)750 void PacketDiagram::showFieldsToggled(bool checked)
751 {
752     layout_->setShowFields(checked);
753     setRootNode(root_node_);
754 }
755 
756 // XXX - We have similar code in tcp_stream_dialog and io_graph_dialog. Should this be a common routine?
saveAsTriggered()757 void PacketDiagram::saveAsTriggered()
758 {
759     QString file_name, extension;
760     QDir path(wsApp->lastOpenDir());
761     QString png_filter = tr("Portable Network Graphics (*.png)");
762     QString bmp_filter = tr("Windows Bitmap (*.bmp)");
763     // Gaze upon my beautiful graph with lossy artifacts!
764     QString jpeg_filter = tr("JPEG File Interchange Format (*.jpeg *.jpg)");
765     QStringList fl = QStringList() << png_filter << bmp_filter << jpeg_filter;
766 #if defined(QT_SVG_LIB) && 0
767     QString svg_filter = tr("Scalable Vector Graphics (*.svg)");
768     fl << svg_filter;
769 #endif
770     QString filter = fl.join(";;");
771 
772     file_name = WiresharkFileDialog::getSaveFileName(this, wsApp->windowTitleString(tr("Save Graph As…")),
773                                              path.canonicalPath(), filter, &extension);
774 
775     if (file_name.length() > 0) {
776         bool save_ok = false;
777         if (extension.compare(png_filter) == 0) {
778             QImage raster_diagram = exportToImage();
779             save_ok = raster_diagram.save(file_name, "PNG");
780         } else if (extension.compare(bmp_filter) == 0) {
781             QImage raster_diagram = exportToImage();
782             save_ok = raster_diagram.save(file_name, "BMP");
783         } else if (extension.compare(jpeg_filter) == 0) {
784             QImage raster_diagram = exportToImage();
785             save_ok = raster_diagram.save(file_name, "JPG");
786         }
787 #if defined(QT_SVG_LIB) && 0
788         else if (extension.compare(svg_filter) == 0) {
789             QByteArray svg_diagram = exportToSvg();
790             QFile file(file_name);
791             if (file.open(QIODevice::WriteOnly)) {
792                 save_ok = file.write(svg_diagram) > 0;
793                 file.close();
794             }
795         }
796 #endif
797         // else error dialog?
798         if (save_ok) {
799             wsApp->setLastOpenDirFromFilename(file_name);
800         }
801     }
802 }
803 
copyAsRasterTriggered()804 void PacketDiagram::copyAsRasterTriggered()
805 {
806     QImage raster_diagram = exportToImage();
807     wsApp->clipboard()->setImage(raster_diagram);
808 }
809 
810 #if defined(QT_SVG_LIB) && !defined(Q_OS_MAC) && 0
copyAsSvgTriggered()811 void PacketDiagram::copyAsSvgTriggered()
812 {
813     QByteArray svg_ba = exportToSvg();
814 
815     // XXX It looks like we have to use/subclass QMacPasteboardMime in
816     // order for this to work on macOS.
817     // It might be easier to just do "Save As" instead.
818     QMimeData *md = new QMimeData();
819     md->setData("image/svg+xml", svg_buf);
820     wsApp->clipboard()->setMimeData(md);
821 }
822 #endif
823