]> sigrok.org Git - pulseview.git/blob - pv/widgets/popup.cpp
Segment: Include <memory> so we don't get error at compile time
[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 #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
256         const QRect screen_rect = QApplication::screenAt(point_)->availableGeometry();
257 #else
258         const QRect screen_rect = QApplication::desktop()->availableGeometry(
259                 QApplication::desktop()->screenNumber(point_));
260 #endif
261
262         if (pos_ == Right || pos_ == Left)
263                 o.ry() = -height() / 2;
264         else
265                 o.rx() = -width() / 2;
266
267         if (pos_ == Left)
268                 o.rx() = -width();
269         else if (pos_ == Top)
270                 o.ry() = -height();
271
272         o += point_;
273         move(max(min(o.x(), screen_rect.right() - width()),
274                         screen_rect.left()),
275                 max(min(o.y(), screen_rect.bottom() - height()),
276                         screen_rect.top()));
277 }
278
279 void Popup::closeEvent(QCloseEvent*)
280 {
281         closed();
282 }
283
284 void Popup::paintEvent(QPaintEvent*)
285 {
286         QPainter painter(this);
287         painter.setRenderHint(QPainter::Antialiasing);
288
289         const QColor outline_color(QApplication::palette().color(
290                 QPalette::Dark));
291
292         // Draw the bubble
293         const QRegion b = bubble_region();
294         const QRegion bubble_outline = QRegion(rect()).subtracted(
295                 b.translated(1, 0).intersected(b.translated(0, 1).intersected(
296                 b.translated(-1, 0).intersected(b.translated(0, -1)))));
297
298         painter.setPen(Qt::NoPen);
299         painter.setBrush(QApplication::palette().brush(QPalette::Window));
300         painter.drawRect(rect());
301
302         // Draw the arrow
303         if (!space_for_arrow())
304                 return;
305
306         const QPoint ArrowOffsets[] = {
307                 QPoint(1, 0), QPoint(0, -1), QPoint(-1, 0), QPoint(0, 1)};
308
309         const QRegion a(arrow_region());
310         const QRegion arrow_outline = a.subtracted(
311                 a.translated(ArrowOffsets[pos_]));
312
313         painter.setClipRegion(bubble_outline.subtracted(a).united(
314                 arrow_outline));
315         painter.setBrush(outline_color);
316         painter.drawRect(rect());
317 }
318
319 void Popup::resizeEvent(QResizeEvent*)
320 {
321         reposition_widget();
322         setMask(popup_region());
323 }
324
325 void Popup::mouseReleaseEvent(QMouseEvent *event)
326 {
327         assert(event);
328
329         // We need our own out-of-bounds click handler because QWidget counts
330         // the drop-shadow region as inside the widget
331         if (!bubble_rect().contains(event->pos()))
332                 close();
333 }
334
335 void Popup::showEvent(QShowEvent*)
336 {
337         reposition_widget();
338 }
339
340 } // namespace widgets
341 } // namespace pv