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