VirtualBox

source: vbox/trunk/src/VBox/Frontends/VirtualBox/src/widgets/UIPopupPane.cpp@ 100347

Last change on this file since 100347 was 98103, checked in by vboxsync, 21 months ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 18.0 KB
Line 
1/* $Id: UIPopupPane.cpp 98103 2023-01-17 14:15:46Z vboxsync $ */
2/** @file
3 * VBox Qt GUI - UIPopupPane class implementation.
4 */
5
6/*
7 * Copyright (C) 2013-2023 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28/* Qt includes: */
29#include <QPainter>
30#include <QTextEdit>
31
32/* GUI includes: */
33#include "UIPopupPane.h"
34#include "UIPopupPaneMessage.h"
35#include "UIPopupPaneDetails.h"
36#include "UIPopupPaneButtonPane.h"
37#include "UIAnimationFramework.h"
38#include "QIMessageBox.h"
39
40/* Other VBox includes: */
41#include <iprt/assert.h>
42
43
44UIPopupPane::UIPopupPane(QWidget *pParent,
45 const QString &strMessage, const QString &strDetails,
46 const QMap<int, QString> &buttonDescriptions)
47 : QIWithRetranslateUI<QWidget>(pParent)
48 , m_fPolished(false)
49 , m_iLayoutMargin(10), m_iLayoutSpacing(5)
50 , m_strMessage(strMessage), m_strDetails(strDetails)
51 , m_buttonDescriptions(buttonDescriptions)
52 , m_fShown(false)
53 , m_pShowAnimation(0)
54 , m_fCanLooseFocus(!m_buttonDescriptions.isEmpty())
55 , m_fFocused(!m_fCanLooseFocus)
56 , m_fHovered(m_fFocused)
57 , m_iDefaultOpacity(180)
58 , m_iHoveredOpacity(250)
59 , m_iOpacity(m_fHovered ? m_iHoveredOpacity : m_iDefaultOpacity)
60 , m_pMessagePane(0), m_pDetailsPane(0), m_pButtonPane(0)
61{
62 /* Prepare: */
63 prepare();
64}
65
66void UIPopupPane::recall()
67{
68 /* Close popup-pane with *escape* button: */
69 done(m_pButtonPane->escapeButton());
70}
71
72void UIPopupPane::setMessage(const QString &strMessage)
73{
74 /* Make sure the message has changed: */
75 if (m_strMessage == strMessage)
76 return;
77
78 /* Fetch new message: */
79 m_strMessage = strMessage;
80 m_pMessagePane->setText(m_strMessage);
81}
82
83void UIPopupPane::setDetails(const QString &strDetails)
84{
85 /* Make sure the details has changed: */
86 if (m_strDetails == strDetails)
87 return;
88
89 /* Fetch new details: */
90 m_strDetails = strDetails;
91 m_pDetailsPane->setText(prepareDetailsText());
92}
93
94void UIPopupPane::setMinimumSizeHint(const QSize &minimumSizeHint)
95{
96 /* Make sure the size-hint has changed: */
97 if (m_minimumSizeHint == minimumSizeHint)
98 return;
99
100 /* Fetch new size-hint: */
101 m_minimumSizeHint = minimumSizeHint;
102
103 /* Notify parent popup-stack: */
104 emit sigSizeHintChanged();
105}
106
107void UIPopupPane::layoutContent()
108{
109 /* Variables: */
110 const int iWidth = width();
111 const int iHeight = height();
112 const QSize buttonPaneMinimumSizeHint = m_pButtonPane->minimumSizeHint();
113 const int iButtonPaneMinimumWidth = buttonPaneMinimumSizeHint.width();
114 const int iButtonPaneMinimumHeight = buttonPaneMinimumSizeHint.height();
115 const int iTextPaneWidth = iWidth - 2 * m_iLayoutMargin - m_iLayoutSpacing - iButtonPaneMinimumWidth;
116 const int iTextPaneHeight = m_pMessagePane->minimumSizeHint().height();
117 const int iMaximumHeight = qMax(iTextPaneHeight, iButtonPaneMinimumHeight);
118 const int iMinimumHeight = qMin(iTextPaneHeight, iButtonPaneMinimumHeight);
119 const int iHeightShift = (iMaximumHeight - iMinimumHeight) / 2;
120 const bool fTextPaneShifted = iTextPaneHeight < iButtonPaneMinimumHeight;
121 const int iTextPaneYOffset = fTextPaneShifted ? m_iLayoutMargin + iHeightShift : m_iLayoutMargin;
122
123 /* Message-pane: */
124 m_pMessagePane->move(m_iLayoutMargin, iTextPaneYOffset);
125 m_pMessagePane->resize(iTextPaneWidth, iTextPaneHeight);
126 m_pMessagePane->layoutContent();
127
128 /* Button-pane: */
129 m_pButtonPane->move(m_iLayoutMargin + iTextPaneWidth + m_iLayoutSpacing,
130 m_iLayoutMargin);
131 m_pButtonPane->resize(iButtonPaneMinimumWidth,
132 iHeight - m_iLayoutSpacing);
133
134 /* Details-pane: */
135 if (m_pDetailsPane->isVisible())
136 {
137 m_pDetailsPane->move(m_iLayoutMargin,
138 iTextPaneYOffset + iTextPaneHeight + m_iLayoutSpacing);
139 m_pDetailsPane->resize(iTextPaneWidth + iButtonPaneMinimumWidth,
140 m_pDetailsPane->minimumSizeHint().height());
141 m_pDetailsPane->layoutContent();
142 }
143}
144
145void UIPopupPane::sltMarkAsShown()
146{
147 /* Mark popup-pane as 'shown': */
148 m_fShown = true;
149}
150
151void UIPopupPane::sltHandleProposalForSize(QSize newSize)
152{
153 /* Prepare the width: */
154 int iWidth = newSize.width();
155
156 /* Subtract layout margins: */
157 iWidth -= 2 * m_iLayoutMargin;
158 /* Subtract layout spacing: */
159 iWidth -= m_iLayoutSpacing;
160 /* Subtract button-pane width: */
161 iWidth -= m_pButtonPane->minimumSizeHint().width();
162
163 /* Propose resulting width to the panes: */
164 emit sigProposePaneWidth(iWidth);
165
166 /* Prepare the height: */
167 int iHeight = newSize.height();
168 /* Determine maximum height of the message-pane / button-pane: */
169 int iExtraHeight = qMax(m_pMessagePane->expandedSizeHint().height(),
170 m_pButtonPane->minimumSizeHint().height());
171
172 /* Subtract height of the message pane: */
173 iHeight -= iExtraHeight;
174 /* Subtract layout margins: */
175 iHeight -= 2 * m_iLayoutMargin;
176 /* Subtract layout spacing: */
177 iHeight -= m_iLayoutSpacing;
178
179 /* Propose resulting height to details-pane: */
180 emit sigProposeDetailsPaneHeight(iHeight);
181}
182
183void UIPopupPane::sltUpdateSizeHint()
184{
185 /* Calculate minimum width-hint: */
186 int iMinimumWidthHint = 0;
187 {
188 /* Take into account layout: */
189 iMinimumWidthHint += 2 * m_iLayoutMargin;
190 {
191 /* Take into account widgets: */
192 iMinimumWidthHint += m_pMessagePane->minimumSizeHint().width();
193 iMinimumWidthHint += m_iLayoutSpacing;
194 iMinimumWidthHint += m_pButtonPane->minimumSizeHint().width();
195 }
196 }
197
198 /* Calculate minimum height-hint: */
199 int iMinimumHeightHint = 0;
200 {
201 /* Take into account layout: */
202 iMinimumHeightHint += 2 * m_iLayoutMargin;
203 iMinimumHeightHint += m_iLayoutSpacing;
204 {
205 /* Take into account widgets: */
206 const int iTextPaneHeight = m_pMessagePane->minimumSizeHint().height();
207 const int iButtonBoxHeight = m_pButtonPane->minimumSizeHint().height();
208 iMinimumHeightHint += qMax(iTextPaneHeight, iButtonBoxHeight);
209 /* Add the height of details-pane only if it is visible: */
210 if (m_pDetailsPane->isVisible())
211 iMinimumHeightHint += m_pDetailsPane->minimumSizeHint().height();
212 }
213 }
214
215 /* Compose minimum size-hints: */
216 m_hiddenSizeHint = QSize(iMinimumWidthHint, 1);
217 m_shownSizeHint = QSize(iMinimumWidthHint, iMinimumHeightHint);
218 m_minimumSizeHint = m_fShown ? m_shownSizeHint : m_hiddenSizeHint;
219
220 /* Update 'show/hide' animation: */
221 if (m_pShowAnimation)
222 m_pShowAnimation->update();
223
224 /* Notify parent popup-stack: */
225 emit sigSizeHintChanged();
226}
227
228void UIPopupPane::sltButtonClicked(int iButtonID)
229{
230 /* Complete popup with corresponding code: */
231 done(iButtonID);
232}
233
234void UIPopupPane::prepare()
235{
236 /* Prepare this: */
237 installEventFilter(this);
238 /* Prepare background: */
239 prepareBackground();
240 /* Prepare content: */
241 prepareContent();
242 /* Prepare animation: */
243 prepareAnimation();
244
245 /* Update size-hint: */
246 sltUpdateSizeHint();
247}
248
249void UIPopupPane::prepareBackground()
250{
251 /* Prepare palette: */
252 QPalette pal = QApplication::palette();
253 pal.setColor(QPalette::Window, QApplication::palette().color(QPalette::Window));
254 setPalette(pal);
255}
256
257void UIPopupPane::prepareContent()
258{
259 /* Create message-pane: */
260 m_pMessagePane = new UIPopupPaneMessage(this, m_strMessage, m_fFocused);
261 {
262 /* Configure message-pane: */
263 connect(this, &UIPopupPane::sigProposePaneWidth, m_pMessagePane, &UIPopupPaneMessage::sltHandleProposalForWidth);
264 connect(m_pMessagePane, &UIPopupPaneMessage::sigSizeHintChanged, this, &UIPopupPane::sltUpdateSizeHint);
265 m_pMessagePane->installEventFilter(this);
266 }
267
268 /* Create button-box: */
269 m_pButtonPane = new UIPopupPaneButtonPane(this);
270 {
271 /* Configure button-box: */
272 connect(m_pButtonPane, &UIPopupPaneButtonPane::sigButtonClicked, this, &UIPopupPane::sltButtonClicked);
273 m_pButtonPane->installEventFilter(this);
274 m_pButtonPane->setButtons(m_buttonDescriptions);
275 }
276
277 /* Create details-pane: */
278 m_pDetailsPane = new UIPopupPaneDetails(this, prepareDetailsText(), m_fFocused);
279 {
280 /* Configure details-pane: */
281 connect(this, &UIPopupPane::sigProposePaneWidth, m_pDetailsPane, &UIPopupPaneDetails::sltHandleProposalForWidth);
282 connect(this, &UIPopupPane::sigProposeDetailsPaneHeight, m_pDetailsPane, &UIPopupPaneDetails::sltHandleProposalForHeight);
283 connect(m_pDetailsPane, &UIPopupPaneDetails::sigSizeHintChanged, this, &UIPopupPane::sltUpdateSizeHint);
284 m_pDetailsPane->installEventFilter(this);
285 }
286
287 /* Prepare focus rules: */
288 setFocusPolicy(Qt::StrongFocus);
289 m_pMessagePane->setFocusPolicy(Qt::StrongFocus);
290 m_pButtonPane->setFocusPolicy(Qt::StrongFocus);
291 m_pDetailsPane->setFocusPolicy(Qt::StrongFocus);
292 setFocusProxy(m_pButtonPane);
293 m_pMessagePane->setFocusProxy(m_pButtonPane);
294 m_pDetailsPane->setFocusProxy(m_pButtonPane);
295
296 /* Translate UI finally: */
297 retranslateUi();
298}
299
300void UIPopupPane::prepareAnimation()
301{
302 /* Install 'show' animation for 'minimumSizeHint' property: */
303 connect(this, SIGNAL(sigToShow()), this, SIGNAL(sigShow()), Qt::QueuedConnection);
304 m_pShowAnimation = UIAnimation::installPropertyAnimation(this, "minimumSizeHint", "hiddenSizeHint", "shownSizeHint",
305 SIGNAL(sigShow()), SIGNAL(sigHide()));
306 connect(m_pShowAnimation, &UIAnimation::sigStateEnteredFinal, this, &UIPopupPane::sltMarkAsShown);
307
308 /* Install 'hover' animation for 'opacity' property: */
309 UIAnimation::installPropertyAnimation(this, "opacity", "defaultOpacity", "hoveredOpacity",
310 SIGNAL(sigHoverEnter()), SIGNAL(sigHoverLeave()), m_fHovered);
311}
312
313void UIPopupPane::retranslateUi()
314{
315 /* Translate tool-tips: */
316 retranslateToolTips();
317}
318
319void UIPopupPane::retranslateToolTips()
320{
321 /* Translate pane & message-pane tool-tips: */
322 if (m_fFocused)
323 {
324 setToolTip(QString());
325 m_pMessagePane->setToolTip(QString());
326 }
327 else
328 {
329 setToolTip(QApplication::translate("UIPopupCenter", "Click for full details"));
330 m_pMessagePane->setToolTip(QApplication::translate("UIPopupCenter", "Click for full details"));
331 }
332}
333
334bool UIPopupPane::eventFilter(QObject *pObject, QEvent *pEvent)
335{
336 /* Handle events for allowed widgets only: */
337 if ( pObject != this
338 && pObject != m_pMessagePane
339 && pObject != m_pButtonPane
340 && pObject != m_pDetailsPane)
341 return QIWithRetranslateUI<QWidget>::eventFilter(pObject, pEvent);
342
343 /* Depending on event-type: */
344 switch (pEvent->type())
345 {
346 /* Something is hovered: */
347 case QEvent::HoverEnter:
348 case QEvent::Enter:
349 {
350 /* Hover pane if not yet hovered: */
351 if (!m_fHovered)
352 {
353 m_fHovered = true;
354 emit sigHoverEnter();
355 }
356 break;
357 }
358 /* Nothing is hovered: */
359 case QEvent::Leave:
360 {
361 /* Unhover pane if hovered but not focused: */
362 if (pObject == this && m_fHovered && !m_fFocused)
363 {
364 m_fHovered = false;
365 emit sigHoverLeave();
366 }
367 break;
368 }
369 /* Pane is clicked with mouse: */
370 case QEvent::MouseButtonPress:
371 {
372 /* Focus pane if not focused: */
373 if (!m_fFocused)
374 {
375 m_fFocused = true;
376 emit sigFocusEnter();
377 /* Hover pane if not hovered: */
378 if (!m_fHovered)
379 {
380 m_fHovered = true;
381 emit sigHoverEnter();
382 }
383 /* Translate tool-tips: */
384 retranslateToolTips();
385 }
386 break;
387 }
388 /* Pane is unfocused: */
389 case QEvent::FocusOut:
390 {
391 /* Unhocus pane if focused: */
392 if (m_fCanLooseFocus && m_fFocused)
393 {
394 m_fFocused = false;
395 emit sigFocusLeave();
396 /* Unhover pane if hovered: */
397 if (m_fHovered)
398 {
399 m_fHovered = false;
400 emit sigHoverLeave();
401 }
402 /* Translate tool-tips: */
403 retranslateToolTips();
404 }
405 break;
406 }
407 /* Default case: */
408 default: break;
409 }
410
411 /* Call to base-class: */
412 return QIWithRetranslateUI<QWidget>::eventFilter(pObject, pEvent);
413}
414
415void UIPopupPane::showEvent(QShowEvent *pEvent)
416{
417 /* Call to base-class: */
418 QWidget::showEvent(pEvent);
419
420 /* Polish border: */
421 if (m_fPolished)
422 return;
423 m_fPolished = true;
424
425 /* Call to polish event: */
426 polishEvent(pEvent);
427}
428
429void UIPopupPane::polishEvent(QShowEvent *)
430{
431 /* Focus if marked as 'focused': */
432 if (m_fFocused)
433 setFocus();
434
435 /* Emit signal to start *show* animation: */
436 emit sigToShow();
437}
438
439void UIPopupPane::paintEvent(QPaintEvent *)
440{
441 /* Compose painting rectangle,
442 * Shifts are required for the antialiasing support: */
443 const QRect rect(1, 1, width() - 2, height() - 2);
444
445 /* Create painter: */
446 QPainter painter(this);
447 painter.setRenderHint(QPainter::Antialiasing);
448
449 /* Configure clipping: */
450 configureClipping(rect, painter);
451
452 /* Paint background: */
453 paintBackground(rect, painter);
454
455 /* Paint frame: */
456 paintFrame(painter);
457}
458
459void UIPopupPane::configureClipping(const QRect &rect, QPainter &painter)
460{
461 /* Configure clipping: */
462 QPainterPath path;
463 int iDiameter = 6;
464 QSizeF arcSize(2 * iDiameter, 2 * iDiameter);
465 path.moveTo(rect.x() + iDiameter, rect.y());
466 path.arcTo(QRectF(path.currentPosition(), arcSize).translated(-iDiameter, 0), 90, 90);
467 path.lineTo(path.currentPosition().x(), rect.y() + rect.height() - iDiameter);
468 path.arcTo(QRectF(path.currentPosition(), arcSize).translated(0, -iDiameter), 180, 90);
469 path.lineTo(rect.x() + rect.width() - iDiameter, path.currentPosition().y());
470 path.arcTo(QRectF(path.currentPosition(), arcSize).translated(-iDiameter, -2 * iDiameter), 270, 90);
471 path.lineTo(path.currentPosition().x(), rect.y() + iDiameter);
472 path.arcTo(QRectF(path.currentPosition(), arcSize).translated(-2 * iDiameter, -iDiameter), 0, 90);
473 path.closeSubpath();
474 painter.setClipPath(path);
475}
476
477void UIPopupPane::paintBackground(const QRect &rect, QPainter &painter)
478{
479 /* Paint background: */
480 QColor currentColor(palette().color(QPalette::Window));
481 QColor newColor1(currentColor.red(), currentColor.green(), currentColor.blue(), opacity());
482 QColor newColor2 = newColor1.darker(115);
483 QLinearGradient headerGradient(rect.topLeft(), rect.bottomLeft());
484 headerGradient.setColorAt(0, newColor1);
485 headerGradient.setColorAt(1, newColor2);
486 painter.fillRect(rect, headerGradient);
487}
488
489void UIPopupPane::paintFrame(QPainter &painter)
490{
491 /* Paint frame: */
492 QColor currentColor(palette().color(QPalette::Window).darker(150));
493 QPainterPath path = painter.clipPath();
494 painter.setClipping(false);
495 painter.strokePath(path, currentColor);
496}
497
498void UIPopupPane::done(int iResultCode)
499{
500 /* Notify listeners: */
501 emit sigDone(iResultCode);
502}
503
504QString UIPopupPane::prepareDetailsText() const
505{
506 if (m_strDetails.isEmpty())
507 return QString();
508
509 QStringPairList aDetailsList;
510 prepareDetailsList(aDetailsList);
511 if (aDetailsList.isEmpty())
512 return QString();
513
514 if (aDetailsList.size() == 1)
515 return tr("<p><b>Details:</b>") + m_strDetails + "</p>";
516
517 QString strResultText;
518 for (int iListIdx = 0; iListIdx < aDetailsList.size(); ++iListIdx)
519 {
520 strResultText += tr("<p><b>Details:</b> (%1 of %2)").arg(iListIdx + 1).arg(aDetailsList.size());
521 const QString strFirstPart = aDetailsList.at(iListIdx).first;
522 const QString strSecondPart = aDetailsList.at(iListIdx).second;
523 if (strFirstPart.isEmpty())
524 strResultText += strSecondPart + "</p>";
525 else
526 strResultText += QString("%1<br>%2").arg(strFirstPart, strSecondPart) + "</p>";
527 }
528 return strResultText;
529}
530
531void UIPopupPane::prepareDetailsList(QStringPairList &aDetailsList) const
532{
533 if (m_strDetails.isEmpty())
534 return;
535
536 /* Split details into paragraphs: */
537#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
538 QStringList aParagraphs(m_strDetails.split("<!--EOP-->", Qt::SkipEmptyParts));
539#else
540 QStringList aParagraphs(m_strDetails.split("<!--EOP-->", QString::SkipEmptyParts));
541#endif
542 /* Make sure details-text has at least one paragraph: */
543 AssertReturnVoid(!aParagraphs.isEmpty());
544
545 /* Enumerate all the paragraphs: */
546 foreach (const QString &strParagraph, aParagraphs)
547 {
548 /* Split each paragraph into pairs: */
549#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
550 QStringList aParts(strParagraph.split("<!--EOM-->", Qt::KeepEmptyParts));
551#else
552 QStringList aParts(strParagraph.split("<!--EOM-->", QString::KeepEmptyParts));
553#endif
554 /* Make sure each paragraph consist of 2 parts: */
555 AssertReturnVoid(aParts.size() == 2);
556 /* Append each pair into details-list: */
557 aDetailsList << QStringPair(aParts.at(0), aParts.at(1));
558 }
559}
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette