VirtualBox

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

Last change on this file was 104358, checked in by vboxsync, 4 weeks 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
Line 
1/* $Id: UIPopupPane.cpp 104358 2024-04-18 05:33:40Z 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 <QApplication>
30#include <QPainter>
31#include <QPainterPath>
32#include <QTextEdit>
33
34/* GUI includes: */
35#include "QIMessageBox.h"
36#include "UIAnimationFramework.h"
37#include "UIPopupPane.h"
38#include "UIPopupPaneMessage.h"
39#include "UIPopupPaneDetails.h"
40#include "UIPopupPaneButtonPane.h"
41#include "UITranslationEventListener.h"
42
43/* Other VBox includes: */
44#include <iprt/assert.h>
45
46
47UIPopupPane::UIPopupPane(QWidget *pParent,
48 const QString &strMessage, const QString &strDetails,
49 const QMap<int, QString> &buttonDescriptions)
50 : QWidget(pParent)
51 , m_fPolished(false)
52 , m_iLayoutMargin(10), m_iLayoutSpacing(5)
53 , m_strMessage(strMessage), m_strDetails(strDetails)
54 , m_buttonDescriptions(buttonDescriptions)
55 , m_fShown(false)
56 , m_pShowAnimation(0)
57 , m_fCanLooseFocus(!m_buttonDescriptions.isEmpty())
58 , m_fFocused(!m_fCanLooseFocus)
59 , m_fHovered(m_fFocused)
60 , m_iDefaultOpacity(180)
61 , m_iHoveredOpacity(250)
62 , m_iOpacity(m_fHovered ? m_iHoveredOpacity : m_iDefaultOpacity)
63 , m_pMessagePane(0), m_pDetailsPane(0), m_pButtonPane(0)
64{
65 /* Prepare: */
66 prepare();
67}
68
69void UIPopupPane::recall()
70{
71 /* Close popup-pane with *escape* button: */
72 done(m_pButtonPane->escapeButton());
73}
74
75void UIPopupPane::setMessage(const QString &strMessage)
76{
77 /* Make sure the message has changed: */
78 if (m_strMessage == strMessage)
79 return;
80
81 /* Fetch new message: */
82 m_strMessage = strMessage;
83 m_pMessagePane->setText(m_strMessage);
84}
85
86void UIPopupPane::setDetails(const QString &strDetails)
87{
88 /* Make sure the details has changed: */
89 if (m_strDetails == strDetails)
90 return;
91
92 /* Fetch new details: */
93 m_strDetails = strDetails;
94 m_pDetailsPane->setText(prepareDetailsText());
95}
96
97void UIPopupPane::setMinimumSizeHint(const QSize &minimumSizeHint)
98{
99 /* Make sure the size-hint has changed: */
100 if (m_minimumSizeHint == minimumSizeHint)
101 return;
102
103 /* Fetch new size-hint: */
104 m_minimumSizeHint = minimumSizeHint;
105
106 /* Notify parent popup-stack: */
107 emit sigSizeHintChanged();
108}
109
110void UIPopupPane::layoutContent()
111{
112 /* Variables: */
113 const int iWidth = width();
114 const int iHeight = height();
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;
119 const int iTextPaneHeight = m_pMessagePane->minimumSizeHint().height();
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;
124 const int iTextPaneYOffset = fTextPaneShifted ? m_iLayoutMargin + iHeightShift : m_iLayoutMargin;
125
126 /* Message-pane: */
127 m_pMessagePane->move(m_iLayoutMargin, iTextPaneYOffset);
128 m_pMessagePane->resize(iTextPaneWidth, iTextPaneHeight);
129 m_pMessagePane->layoutContent();
130
131 /* Button-pane: */
132 m_pButtonPane->move(m_iLayoutMargin + iTextPaneWidth + m_iLayoutSpacing,
133 m_iLayoutMargin);
134 m_pButtonPane->resize(iButtonPaneMinimumWidth,
135 iHeight - m_iLayoutSpacing);
136
137 /* Details-pane: */
138 if (m_pDetailsPane->isVisible())
139 {
140 m_pDetailsPane->move(m_iLayoutMargin,
141 iTextPaneYOffset + iTextPaneHeight + m_iLayoutSpacing);
142 m_pDetailsPane->resize(iTextPaneWidth + iButtonPaneMinimumWidth,
143 m_pDetailsPane->minimumSizeHint().height());
144 m_pDetailsPane->layoutContent();
145 }
146}
147
148void UIPopupPane::sltMarkAsShown()
149{
150 /* Mark popup-pane as 'shown': */
151 m_fShown = true;
152}
153
154void UIPopupPane::sltHandleProposalForSize(QSize newSize)
155{
156 /* Prepare the width: */
157 int iWidth = newSize.width();
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
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);
184}
185
186void UIPopupPane::sltUpdateSizeHint()
187{
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: */
195 iMinimumWidthHint += m_pMessagePane->minimumSizeHint().width();
196 iMinimumWidthHint += m_iLayoutSpacing;
197 iMinimumWidthHint += m_pButtonPane->minimumSizeHint().width();
198 }
199 }
200
201 /* Calculate minimum height-hint: */
202 int iMinimumHeightHint = 0;
203 {
204 /* Take into account layout: */
205 iMinimumHeightHint += 2 * m_iLayoutMargin;
206 iMinimumHeightHint += m_iLayoutSpacing;
207 {
208 /* Take into account widgets: */
209 const int iTextPaneHeight = m_pMessagePane->minimumSizeHint().height();
210 const int iButtonBoxHeight = m_pButtonPane->minimumSizeHint().height();
211 iMinimumHeightHint += qMax(iTextPaneHeight, iButtonBoxHeight);
212 /* Add the height of details-pane only if it is visible: */
213 if (m_pDetailsPane->isVisible())
214 iMinimumHeightHint += m_pDetailsPane->minimumSizeHint().height();
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
227 /* Notify parent popup-stack: */
228 emit sigSizeHintChanged();
229}
230
231void UIPopupPane::sltButtonClicked(int iButtonID)
232{
233 /* Complete popup with corresponding code: */
234 done(iButtonID);
235}
236
237void UIPopupPane::prepare()
238{
239 /* Prepare this: */
240 installEventFilter(this);
241 /* Prepare background: */
242 prepareBackground();
243 /* Prepare content: */
244 prepareContent();
245 /* Prepare animation: */
246 prepareAnimation();
247
248 /* Update size-hint: */
249 sltUpdateSizeHint();
250}
251
252void UIPopupPane::prepareBackground()
253{
254 /* Prepare palette: */
255 QPalette pal = QApplication::palette();
256 pal.setColor(QPalette::Window, QApplication::palette().color(QPalette::Window));
257 setPalette(pal);
258}
259
260void UIPopupPane::prepareContent()
261{
262 /* Create message-pane: */
263 m_pMessagePane = new UIPopupPaneMessage(this, m_strMessage, m_fFocused);
264 {
265 /* Configure message-pane: */
266 connect(this, &UIPopupPane::sigProposePaneWidth, m_pMessagePane, &UIPopupPaneMessage::sltHandleProposalForWidth);
267 connect(m_pMessagePane, &UIPopupPaneMessage::sigSizeHintChanged, this, &UIPopupPane::sltUpdateSizeHint);
268 m_pMessagePane->installEventFilter(this);
269 }
270
271 /* Create button-box: */
272 m_pButtonPane = new UIPopupPaneButtonPane(this);
273 {
274 /* Configure button-box: */
275 connect(m_pButtonPane, &UIPopupPaneButtonPane::sigButtonClicked, this, &UIPopupPane::sltButtonClicked);
276 m_pButtonPane->installEventFilter(this);
277 m_pButtonPane->setButtons(m_buttonDescriptions);
278 }
279
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
290 /* Prepare focus rules: */
291 setFocusPolicy(Qt::StrongFocus);
292 m_pMessagePane->setFocusPolicy(Qt::StrongFocus);
293 m_pButtonPane->setFocusPolicy(Qt::StrongFocus);
294 m_pDetailsPane->setFocusPolicy(Qt::StrongFocus);
295 setFocusProxy(m_pButtonPane);
296 m_pMessagePane->setFocusProxy(m_pButtonPane);
297 m_pDetailsPane->setFocusProxy(m_pButtonPane);
298
299 /* Translate UI finally: */
300 sltRetranslateUI();
301 connect(&translationEventListener(), &UITranslationEventListener::sigRetranslateUI,
302 this, &UIPopupPane::sltRetranslateUI);
303}
304
305void UIPopupPane::prepareAnimation()
306{
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()));
311 connect(m_pShowAnimation, &UIAnimation::sigStateEnteredFinal, this, &UIPopupPane::sltMarkAsShown);
312
313 /* Install 'hover' animation for 'opacity' property: */
314 UIAnimation::installPropertyAnimation(this, "opacity", "defaultOpacity", "hoveredOpacity",
315 SIGNAL(sigHoverEnter()), SIGNAL(sigHoverLeave()), m_fHovered);
316}
317
318void UIPopupPane::sltRetranslateUI()
319{
320 /* Translate tool-tips: */
321 retranslateToolTips();
322}
323
324void UIPopupPane::retranslateToolTips()
325{
326 /* Translate pane & message-pane tool-tips: */
327 if (m_fFocused)
328 {
329 setToolTip(QString());
330 m_pMessagePane->setToolTip(QString());
331 }
332 else
333 {
334 setToolTip(QApplication::translate("UIPopupCenter", "Click for full details"));
335 m_pMessagePane->setToolTip(QApplication::translate("UIPopupCenter", "Click for full details"));
336 }
337}
338
339bool UIPopupPane::eventFilter(QObject *pObject, QEvent *pEvent)
340{
341 /* Handle events for allowed widgets only: */
342 if ( pObject != this
343 && pObject != m_pMessagePane
344 && pObject != m_pButtonPane
345 && pObject != m_pDetailsPane)
346 return QWidget::eventFilter(pObject, pEvent);
347
348 /* Depending on event-type: */
349 switch (pEvent->type())
350 {
351 /* Something is hovered: */
352 case QEvent::HoverEnter:
353 case QEvent::Enter:
354 {
355 /* Hover pane if not yet hovered: */
356 if (!m_fHovered)
357 {
358 m_fHovered = true;
359 emit sigHoverEnter();
360 }
361 break;
362 }
363 /* Nothing is hovered: */
364 case QEvent::Leave:
365 {
366 /* Unhover pane if hovered but not focused: */
367 if (pObject == this && m_fHovered && !m_fFocused)
368 {
369 m_fHovered = false;
370 emit sigHoverLeave();
371 }
372 break;
373 }
374 /* Pane is clicked with mouse: */
375 case QEvent::MouseButtonPress:
376 {
377 /* Focus pane if not focused: */
378 if (!m_fFocused)
379 {
380 m_fFocused = true;
381 emit sigFocusEnter();
382 /* Hover pane if not hovered: */
383 if (!m_fHovered)
384 {
385 m_fHovered = true;
386 emit sigHoverEnter();
387 }
388 /* Translate tool-tips: */
389 retranslateToolTips();
390 }
391 break;
392 }
393 /* Pane is unfocused: */
394 case QEvent::FocusOut:
395 {
396 /* Unhocus pane if focused: */
397 if (m_fCanLooseFocus && m_fFocused)
398 {
399 m_fFocused = false;
400 emit sigFocusLeave();
401 /* Unhover pane if hovered: */
402 if (m_fHovered)
403 {
404 m_fHovered = false;
405 emit sigHoverLeave();
406 }
407 /* Translate tool-tips: */
408 retranslateToolTips();
409 }
410 break;
411 }
412 /* Default case: */
413 default: break;
414 }
415
416 /* Call to base-class: */
417 return QWidget::eventFilter(pObject, pEvent);
418}
419
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
434void UIPopupPane::polishEvent(QShowEvent *)
435{
436 /* Focus if marked as 'focused': */
437 if (m_fFocused)
438 setFocus();
439
440 /* Emit signal to start *show* animation: */
441 emit sigToShow();
442}
443
444void UIPopupPane::paintEvent(QPaintEvent *)
445{
446 /* Compose painting rectangle,
447 * Shifts are required for the antialiasing support: */
448 const QRect rect(1, 1, width() - 2, height() - 2);
449
450 /* Create painter: */
451 QPainter painter(this);
452 painter.setRenderHint(QPainter::Antialiasing);
453
454 /* Configure clipping: */
455 configureClipping(rect, painter);
456
457 /* Paint background: */
458 paintBackground(rect, painter);
459
460 /* Paint frame: */
461 paintFrame(painter);
462}
463
464void UIPopupPane::configureClipping(const QRect &rect, QPainter &painter)
465{
466 /* Configure clipping: */
467 QPainterPath path;
468 int iDiameter = 6;
469 QSizeF arcSize(2 * iDiameter, 2 * iDiameter);
470 path.moveTo(rect.x() + iDiameter, rect.y());
471 path.arcTo(QRectF(path.currentPosition(), arcSize).translated(-iDiameter, 0), 90, 90);
472 path.lineTo(path.currentPosition().x(), rect.y() + rect.height() - iDiameter);
473 path.arcTo(QRectF(path.currentPosition(), arcSize).translated(0, -iDiameter), 180, 90);
474 path.lineTo(rect.x() + rect.width() - iDiameter, path.currentPosition().y());
475 path.arcTo(QRectF(path.currentPosition(), arcSize).translated(-iDiameter, -2 * iDiameter), 270, 90);
476 path.lineTo(path.currentPosition().x(), rect.y() + iDiameter);
477 path.arcTo(QRectF(path.currentPosition(), arcSize).translated(-2 * iDiameter, -iDiameter), 0, 90);
478 path.closeSubpath();
479 painter.setClipPath(path);
480}
481
482void UIPopupPane::paintBackground(const QRect &rect, QPainter &painter)
483{
484 /* Paint background: */
485 QColor currentColor(palette().color(QPalette::Window));
486 QColor newColor1(currentColor.red(), currentColor.green(), currentColor.blue(), opacity());
487 QColor newColor2 = newColor1.darker(115);
488 QLinearGradient headerGradient(rect.topLeft(), rect.bottomLeft());
489 headerGradient.setColorAt(0, newColor1);
490 headerGradient.setColorAt(1, newColor2);
491 painter.fillRect(rect, headerGradient);
492}
493
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
503void UIPopupPane::done(int iResultCode)
504{
505 /* Notify listeners: */
506 emit sigDone(iResultCode);
507}
508
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: */
542#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
543 QStringList aParagraphs(m_strDetails.split("<!--EOP-->", Qt::SkipEmptyParts));
544#else
545 QStringList aParagraphs(m_strDetails.split("<!--EOP-->", QString::SkipEmptyParts));
546#endif
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: */
554#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
555 QStringList aParts(strParagraph.split("<!--EOM-->", Qt::KeepEmptyParts));
556#else
557 QStringList aParts(strParagraph.split("<!--EOM-->", QString::KeepEmptyParts));
558#endif
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