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