]> sigrok.org Git - pulseview.git/blob - pv/widgets/popup.cpp
Session: Fix issue #67 by improving error handling
[pulseview.git] / pv / widgets / popup.cpp
1 /*
2  * This file is part of the PulseView project.
3  *
4  * Copyright (C) 2013 Joel Holdsworth <joel@airwebreathe.org.uk>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #include <algorithm>
21 #include <cassert>
22
23 #include <QApplication>
24 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
25 #include <QScreen>
26 #else
27 #include <QDesktopWidget>
28 #endif
29 #include <QLineEdit>
30 #include <QScrollBar>
31 #include <QStyle>
32 #include <QtGui>
33
34 #include "popup.hpp"
35
36 using std::max;
37 using std::min;
38
39 namespace pv {
40 namespace widgets {
41
42 const unsigned int Popup::ArrowLength = 10;
43 const unsigned int Popup::ArrowOverlap = 3;
44 const unsigned int Popup::MarginWidth = 6;
45
46
47 QWidthAdjustingScrollArea::QWidthAdjustingScrollArea(QWidget* parent) :
48         QScrollArea(parent)
49 {
50 }
51
52 void QWidthAdjustingScrollArea::setWidget(QWidget* w)
53 {
54         QScrollArea::setWidget(w);
55         // It happens that QScrollArea already filters widget events,
56         // but that's an implementation detail that we shouldn't rely on.
57         w->installEventFilter(this);
58 }
59
60 bool QWidthAdjustingScrollArea::eventFilter(QObject* obj, QEvent* ev)
61 {
62         if (obj == widget() && ev->type() == QEvent::Resize) {
63                 if (widget()->height() > height())
64                         setMinimumWidth(widget()->width() + qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent));
65                 else
66                         setMinimumWidth(widget()->width());
67         }
68
69         return QScrollArea::eventFilter(obj, ev);
70 }
71
72
73 Popup::Popup(QWidget *parent) :
74         QWidget(parent, Qt::Popup | Qt::FramelessWindowHint),
75         point_(),
76         pos_(Left)
77 {
78 }
79
80 const QPoint& Popup::point() const
81 {
82         return point_;
83 }
84
85 Popup::Position Popup::position() const
86 {
87         return pos_;
88 }
89
90 void Popup::set_position(const QPoint point, Position pos)
91 {
92         point_ = point, pos_ = pos;
93
94         setContentsMargins(
95                 MarginWidth + ((pos == Right) ? ArrowLength : 0),
96                 MarginWidth + ((pos == Bottom) ? ArrowLength : 0),
97                 MarginWidth + ((pos == Left) ? ArrowLength : 0),
98                 MarginWidth + ((pos == Top) ? ArrowLength : 0));
99 }
100
101 bool Popup::eventFilter(QObject *obj, QEvent *event)
102 {
103         QKeyEvent *keyEvent;
104
105         (void)obj;
106
107         if (event->type() == QEvent::KeyPress) {
108                 keyEvent = static_cast<QKeyEvent*>(event);
109                 if (keyEvent->key() == Qt::Key_Enter ||
110                     keyEvent->key() == Qt::Key_Return) {
111                         close();
112                         return true;
113                 }
114         }
115
116         return false;
117 }
118
119 void Popup::show()
120 {
121         QLineEdit* le;
122
123         QWidget::show();
124
125         // We want to close the popup when the Enter key was
126         // pressed and the first editable widget had focus.
127         if ((le = this->findChild<QLineEdit*>())) {
128
129                 // For combo boxes we need to hook into the parent of
130                 // the line edit (i.e. the QComboBox). For edit boxes
131                 // we hook into the widget directly.
132                 if (le->parent()->metaObject()->className() ==
133                                 this->metaObject()->className())
134                         le->installEventFilter(this);
135                 else
136                         le->parent()->installEventFilter(this);
137
138                 le->selectAll();
139                 le->setFocus();
140         }
141 }
142
143 bool Popup::space_for_arrow() const
144 {
145         // Check if there is room for the arrow
146         switch (pos_) {
147         case Right:
148                 if (point_.x() > x())
149                         return false;
150                 return true;
151
152         case Bottom:
153                 if (point_.y() > y())
154                         return false;
155                 return true;
156
157         case Left:
158                 if (point_.x() < (x() + width()))
159                         return false;
160                 return true;
161
162         case Top:
163                 if (point_.y() < (y() + height()))
164                         return false;
165                 return true;
166         }
167
168         return true;
169 }
170
171 QPolygon Popup::arrow_polygon() const
172 {
173         QPolygon poly;
174
175         const QPoint p = mapFromGlobal(point_);
176         const int l = ArrowLength + ArrowOverlap;
177
178         switch (pos_) {
179         case Right:
180                 poly << QPoint(p.x() + l, p.y() - l);
181                 break;
182
183         case Bottom:
184                 poly << QPoint(p.x() - l, p.y() + l);
185                 break;
186
187         case Left:
188         case Top:
189                 poly << QPoint(p.x() - l, p.y() - l);
190                 break;
191         }
192
193         poly << p;
194
195         switch (pos_) {
196         case Right:
197         case Bottom:
198                 poly << QPoint(p.x() + l, p.y() + l);
199                 break;
200
201         case Left:
202                 poly << QPoint(p.x() - l, p.y() + l);
203                 break;
204
205         case Top:
206                 poly << QPoint(p.x() + l, p.y() - l);
207                 break;
208         }
209
210         return poly;
211 }
212
213 QRegion Popup::arrow_region() const
214 {
215         return QRegion(arrow_polygon());
216 }
217
218 QRect Popup::bubble_rect() const
219 {
220         return QRect(
221                 QPoint((pos_ == Right) ? ArrowLength : 0,
222                         (pos_ == Bottom) ? ArrowLength : 0),
223                 QSize(width() - ((pos_ == Left || pos_ == Right) ?
224                                 ArrowLength : 0),
225                         height() - ((pos_ == Top || pos_ == Bottom) ?
226                                 ArrowLength : 0)));
227 }
228
229 QRegion Popup::bubble_region() const
230 {
231         const QRect rect(bubble_rect());
232
233         const unsigned int r = MarginWidth;
234         const unsigned int d = 2 * r;
235         return QRegion(rect.adjusted(r, 0, -r, 0)).united(
236                 QRegion(rect.adjusted(0, r, 0, -r))).united(
237                 QRegion(rect.left(), rect.top(), d, d,
238                         QRegion::Ellipse)).united(
239                 QRegion(rect.right() - d, rect.top(), d, d,
240                         QRegion::Ellipse)).united(
241                 QRegion(rect.left(), rect.bottom() - d, d, d,
242                         QRegion::Ellipse)).united(
243                 QRegion(rect.right() - d, rect.bottom() - d, d, d,
244                         QRegion::Ellipse));
245 }
246
247 QRegion Popup::popup_region() const
248 {
249         if (space_for_arrow())
250                 return arrow_region().united(bubble_region());
251         else
252                 return bubble_region();
253 }
254
255 void Popup::reposition_widget()
256 {
257         QPoint o;
258
259 #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
260         const QRect screen_rect = QApplication::screenAt(point_)->availableGeometry();
261 #else
262         const QRect screen_rect = QApplication::desktop()->availableGeometry(
263                 QApplication::desktop()->screenNumber(point_));
264 #endif
265
266         if (pos_ == Right || pos_ == Left)
267                 o.ry() = -height() / 2;
268         else
269                 o.rx() = -width() / 2;
270
271         if (pos_ == Left)
272                 o.rx() = -width();
273         else if (pos_ == Top)
274                 o.ry() = -height();
275
276         o += point_;
277         move(max(min(o.x(), screen_rect.right() - width()),
278                         screen_rect.left()),
279                 max(min(o.y(), screen_rect.bottom() - height()),
280                         screen_rect.top()));
281 }
282
283 void Popup::closeEvent(QCloseEvent*)
284 {
285         closed();
286 }
287
288 void Popup::paintEvent(QPaintEvent*)
289 {
290         QPainter painter(this);
291         painter.setRenderHint(QPainter::Antialiasing);
292
293         const QColor outline_color(QApplication::palette().color(
294                 QPalette::Dark));
295
296         // Draw the bubble
297         const QRegion b = bubble_region();
298         const QRegion bubble_outline = QRegion(rect()).subtracted(
299                 b.translated(1, 0).intersected(b.translated(0, 1).intersected(
300                 b.translated(-1, 0).intersected(b.translated(0, -1)))));
301
302         painter.setPen(Qt::NoPen);
303         painter.setBrush(QApplication::palette().brush(QPalette::Window));
304         painter.drawRect(rect());
305
306         // Draw the arrow
307         if (!space_for_arrow())
308                 return;
309
310         const QPoint ArrowOffsets[] = {
311                 QPoint(1, 0), QPoint(0, -1), QPoint(-1, 0), QPoint(0, 1)};
312
313         const QRegion a(arrow_region());
314         const QRegion arrow_outline = a.subtracted(
315                 a.translated(ArrowOffsets[pos_]));
316
317         painter.setClipRegion(bubble_outline.subtracted(a).united(
318                 arrow_outline));
319         painter.setBrush(outline_color);
320         painter.drawRect(rect());
321 }
322
323 void Popup::resizeEvent(QResizeEvent*)
324 {
325         reposition_widget();
326         setMask(popup_region());
327 }
328
329 void Popup::mouseReleaseEvent(QMouseEvent *event)
330 {
331         assert(event);
332
333         // We need our own out-of-bounds click handler because QWidget counts
334         // the drop-shadow region as inside the widget
335         if (!bubble_rect().contains(event->pos()))
336                 close();
337 }
338
339 void Popup::showEvent(QShowEvent*)
340 {
341         reposition_widget();
342 }
343
344 } // namespace widgets
345 } // namespace pv