VirtualBox

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

Last change on this file was 104358, checked in by vboxsync, 2 months ago

FE/Qt. bugref:10622. More refactoring around the retranslation functionality.

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

© 2023 Oracle
ContactPrivacy policyTerms of Use