1 /* overlay_scroll_bar.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 <ui/qt/widgets/overlay_scroll_bar.h>
11
12 #include <ui/qt/utils/color_utils.h>
13
14 #include <QMouseEvent>
15 #include <QPainter>
16 #include <QResizeEvent>
17 #include <QStyleOptionSlider>
18
19 // To do:
20 // - We could graph something useful (e.g. delay times) in packet_map_img_.
21 // https://www.wireshark.org/lists/ethereal-dev/200011/msg00122.html
22 // - Properly handle transience.
23
24 // We want a normal scrollbar with space on either side on which we can draw
25 // and receive mouse events. Adding space using a stylesheet loses native
26 // styling on Windows. Overriding QProxyStyle::drawComplexControl (which is
27 // called by QScrollBar::paintEvent) results in odd behavior on Windows.
28 //
29 // The best solution so far seems to be to simply create a normal-sized child
30 // scrollbar, manually position it, and synchronize it with its parent. We
31 // can then alter the parent's mouse and paint behavior to our heart's
32 // content.
33
34 class OsbProxyStyle : public QProxyStyle
35 {
36 public:
37 // Disable transient behavior. Mainly for macOS but possibly applies to
38 // other platforms. If we want to enable transience we'll have to
39 // handle the following at a minimum:
40 //
41 // setProperty("visible") from QScrollbarStyleAnimation.
42 // Other visibility changes.
43 // HoverEnter & HoverLeave events from QAbstractScrollArea.
44 // Size (and possibly opacity) changes while painting.
45 //
46 // Another approach would be to flip the child-parent relationship
47 // and make the parent a normal scroll bar with a manually-placed
48 // packet map child. This might make the packet list geometry a bit
49 // wonky, however.
50
styleHint(StyleHint hint,const QStyleOption * option=NULL,const QWidget * widget=NULL,QStyleHintReturn * returnData=NULL) const51 virtual int styleHint(StyleHint hint, const QStyleOption *option = NULL, const QWidget *widget = NULL, QStyleHintReturn *returnData = NULL) const {
52 if (hint == SH_ScrollBar_Transient) return false;
53
54 return QProxyStyle::styleHint(hint, option, widget, returnData);
55 }
56 };
57
OverlayScrollBar(Qt::Orientation orientation,QWidget * parent)58 OverlayScrollBar::OverlayScrollBar(Qt::Orientation orientation, QWidget *parent) :
59 QScrollBar(orientation, parent),
60 child_sb_(orientation, this),
61 packet_map_img_(QImage()),
62 packet_map_width_(0),
63 marked_packet_width_(0),
64 packet_count_(-1),
65 start_pos_(-1),
66 end_pos_(-1),
67 positions_(QList<int>())
68 {
69 style_ = new OsbProxyStyle();
70 setStyle(style_);
71
72 child_style_ = new OsbProxyStyle();
73 child_sb_.raise();
74 child_sb_.installEventFilter(this);
75 child_sb_.setStyle(child_style_);
76
77 // XXX Do we need to connect anything else?
78 connect(this, &OverlayScrollBar::rangeChanged, this, &OverlayScrollBar::setChildRange);
79 connect(this, &OverlayScrollBar::valueChanged, &child_sb_, &QScrollBar::setValue);
80
81 connect(&child_sb_, &QScrollBar::valueChanged, this, &OverlayScrollBar::setValue);
82 connect(&child_sb_, &QScrollBar::actionTriggered, this, &OverlayScrollBar::actionTriggered);
83 }
84
~OverlayScrollBar()85 OverlayScrollBar::~OverlayScrollBar()
86 {
87 delete child_style_;
88 delete style_;
89 }
90
sizeHint() const91 QSize OverlayScrollBar::sizeHint() const
92 {
93 return QSize(packet_map_width_ + child_sb_.sizeHint().width(),
94 QScrollBar::sizeHint().height());
95 }
96
sliderPosition()97 int OverlayScrollBar::sliderPosition()
98 {
99 return child_sb_.sliderPosition();
100 }
101
setNearOverlayImage(QImage & overlay_image,int packet_count,int start_pos,int end_pos,QList<int> positions)102 void OverlayScrollBar::setNearOverlayImage(QImage &overlay_image, int packet_count, int start_pos, int end_pos, QList<int> positions)
103 {
104 int old_width = packet_map_img_.width();
105 packet_map_img_ = overlay_image;
106 packet_count_ = packet_count;
107 start_pos_ = start_pos;
108 end_pos_ = end_pos;
109 positions_ = positions;
110
111 if (old_width != packet_map_img_.width()) {
112 qreal dp_ratio = devicePixelRatio();
113
114 packet_map_width_ = packet_map_img_.width() / dp_ratio;
115
116 updateGeometry();
117 }
118 update();
119 }
120
setMarkedPacketImage(QImage & mp_image)121 void OverlayScrollBar::setMarkedPacketImage(QImage &mp_image)
122 {
123 qreal dp_ratio = devicePixelRatio();
124
125 marked_packet_img_ = mp_image;
126 marked_packet_width_ = mp_image.width() / dp_ratio;
127
128 child_sb_.update();
129 }
130
grooveRect()131 QRect OverlayScrollBar::grooveRect()
132 {
133 QStyleOptionSlider opt;
134
135 initStyleOption(&opt);
136 opt.rect = child_sb_.rect();
137
138 return child_sb_.style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, &child_sb_);
139 }
140
resizeEvent(QResizeEvent * event)141 void OverlayScrollBar::resizeEvent(QResizeEvent *event)
142 {
143 QScrollBar::resizeEvent(event);
144
145 child_sb_.move(packet_map_width_, 0);
146 child_sb_.resize(child_sb_.sizeHint().width(), height());
147 child_sb_.setPageStep(height());
148 }
149
paintEvent(QPaintEvent * event)150 void OverlayScrollBar::paintEvent(QPaintEvent *event)
151 {
152 qreal dp_ratio = devicePixelRatio();
153 QSize pm_size(packet_map_width_, geometry().height());
154 pm_size *= dp_ratio;
155
156 QPainter painter(this);
157
158 painter.fillRect(event->rect(), palette().base());
159
160 if (!packet_map_img_.isNull()) {
161 QImage packet_map(pm_size, QImage::Format_ARGB32_Premultiplied);
162 packet_map.fill(Qt::transparent);
163
164 // Draw the image supplied by the packet list.
165 QPainter pm_painter(&packet_map);
166 pm_painter.setPen(Qt::NoPen);
167
168 QRect near_dest(0, 0, pm_size.width(), pm_size.height());
169 pm_painter.drawImage(near_dest, packet_map_img_.scaled(near_dest.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
170
171 // Selected packet indicator
172 if (positions_.count() > 0)
173 {
174 foreach (int selected_pos_, positions_)
175 {
176 if (selected_pos_ >= 0 && selected_pos_ < packet_map_img_.height()) {
177 pm_painter.save();
178 int no_pos = near_dest.height() * selected_pos_ / packet_map_img_.height();
179 int height = dp_ratio;
180 if ((selected_pos_ + 1) < packet_map_img_.height())
181 {
182 int nx_pos = near_dest.height() * ( selected_pos_ + 1 ) / packet_map_img_.height();
183 height = (nx_pos - no_pos + 1) > dp_ratio ? nx_pos - no_pos + 1 : dp_ratio;
184 }
185 pm_painter.setBrush(palette().highlight().color());
186 pm_painter.drawRect(0, no_pos, pm_size.width(), height);
187 pm_painter.restore();
188 }
189 }
190 }
191
192 // Borders
193 pm_painter.save();
194 QColor border_color(ColorUtils::alphaBlend(palette().text(), palette().window(), 0.25));
195 pm_painter.setPen(border_color);
196 pm_painter.drawLine(near_dest.topLeft(), near_dest.bottomLeft());
197 pm_painter.drawLine(near_dest.topRight(), near_dest.bottomRight());
198 pm_painter.drawLine(near_dest.bottomLeft(), near_dest.bottomRight());
199 pm_painter.restore();
200
201 // Draw the map.
202 packet_map.setDevicePixelRatio(dp_ratio);
203 painter.drawImage(0, 0, packet_map);
204 }
205 }
206
eventFilter(QObject * watched,QEvent * event)207 bool OverlayScrollBar::eventFilter(QObject *watched, QEvent *event)
208 {
209 bool ret = false;
210 if (watched == &child_sb_ && event->type() == QEvent::Paint) {
211 // Paint the scrollbar first.
212 child_sb_.event(event);
213 ret = true;
214
215 if (!marked_packet_img_.isNull()) {
216 QRect groove_rect = grooveRect();
217 qreal dp_ratio = devicePixelRatio();
218 groove_rect.setTopLeft(groove_rect.topLeft() * dp_ratio);
219 groove_rect.setSize(groove_rect.size() * dp_ratio);
220
221 QImage marked_map(groove_rect.width(), groove_rect.height(), QImage::Format_ARGB32_Premultiplied);
222 marked_map.fill(Qt::transparent);
223
224 QPainter mm_painter(&marked_map);
225 mm_painter.setPen(Qt::NoPen);
226
227 QRect far_dest(0, 0, groove_rect.width(), groove_rect.height());
228 mm_painter.drawImage(far_dest, marked_packet_img_.scaled(far_dest.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
229
230 marked_map.setDevicePixelRatio(dp_ratio);
231 QPainter painter(&child_sb_);
232 painter.drawImage(groove_rect.left(), groove_rect.top(), marked_map);
233 }
234 }
235
236 return ret;
237 }
238
mouseReleaseEvent(QMouseEvent * event)239 void OverlayScrollBar::mouseReleaseEvent(QMouseEvent *event)
240 {
241 QRect pm_r(0, 0, packet_map_width_, height());
242
243 if (pm_r.contains(event->pos()) && geometry().height() > 0 && packet_count_ > 0 && pageStep() > 0) {
244 double map_ratio = double(end_pos_ - start_pos_) / geometry().height();
245 int clicked_packet = (event->pos().y() * map_ratio) + start_pos_;
246 double packet_to_sb_value = double(maximum() - minimum()) / packet_count_;
247 int top_pad = pageStep() / 4; // Land near, but not at, the top.
248
249 setValue((clicked_packet * packet_to_sb_value) + top_pad);
250 }
251 }
252