1 | /* $Id: QIMessageBox.cpp 104586 2024-05-13 12:12:44Z vboxsync $ */
|
---|
2 | /** @file
|
---|
3 | * VBox Qt GUI - Qt extensions: QIMessageBox class implementation.
|
---|
4 | */
|
---|
5 |
|
---|
6 | /*
|
---|
7 | * Copyright (C) 2006-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 <QCheckBox>
|
---|
31 | #include <QClipboard>
|
---|
32 | #include <QHBoxLayout>
|
---|
33 | #include <QLabel>
|
---|
34 | #include <QMimeData>
|
---|
35 | #include <QPushButton>
|
---|
36 | #include <QRegularExpression>
|
---|
37 | #include <QStyle>
|
---|
38 | #include <QVBoxLayout>
|
---|
39 |
|
---|
40 | /* GUI includes: */
|
---|
41 | #include "QIArrowSplitter.h"
|
---|
42 | #include "QIDialogButtonBox.h"
|
---|
43 | #include "QIMessageBox.h"
|
---|
44 | #include "QIRichTextLabel.h"
|
---|
45 | #include "UICommon.h"
|
---|
46 | #include "UIIconPool.h"
|
---|
47 | #include "UIHelpBrowserDialog.h"
|
---|
48 | #include "UIMessageCenter.h"
|
---|
49 |
|
---|
50 | /* Other VBox includes: */
|
---|
51 | #include <iprt/assert.h>
|
---|
52 |
|
---|
53 |
|
---|
54 | QIMessageBox::QIMessageBox(const QString &strTitle, const QString &strMessage, AlertIconType iconType,
|
---|
55 | int iButton1 /* = 0*/, int iButton2 /* = 0*/, int iButton3 /* = 0*/, QWidget *pParent /* = 0*/,
|
---|
56 | const QString &strHelpKeyword /* = QString() */)
|
---|
57 | : QIDialog(pParent)
|
---|
58 | , m_strTitle(strTitle)
|
---|
59 | , m_iconType(iconType)
|
---|
60 | , m_pLabelIcon(0)
|
---|
61 | , m_strMessage(strMessage)
|
---|
62 | , m_pLabelText(0)
|
---|
63 | , m_pFlagCheckBox(0)
|
---|
64 | , m_pDetailsContainer(0)
|
---|
65 | , m_iButton1(iButton1)
|
---|
66 | , m_iButton2(iButton2)
|
---|
67 | , m_iButton3(iButton3)
|
---|
68 | , m_iButtonEsc(0)
|
---|
69 | , m_pButton1(0)
|
---|
70 | , m_pButton2(0)
|
---|
71 | , m_pButton3(0)
|
---|
72 | , m_pButtonHelp(0)
|
---|
73 | , m_pButtonBox(0)
|
---|
74 | , m_strHelpKeyword(strHelpKeyword)
|
---|
75 | , m_fDone(false)
|
---|
76 | {
|
---|
77 | /* Prepare: */
|
---|
78 | prepare();
|
---|
79 | }
|
---|
80 |
|
---|
81 | void QIMessageBox::setDetailsText(const QString &strText)
|
---|
82 | {
|
---|
83 | /* Make sure details-text is NOT empty: */
|
---|
84 | AssertReturnVoid(!strText.isEmpty());
|
---|
85 |
|
---|
86 | /* Split details into paragraphs: */
|
---|
87 | QStringList paragraphs(strText.split("<!--EOP-->", Qt::SkipEmptyParts));
|
---|
88 | /* Make sure details-text has at least one paragraph: */
|
---|
89 | AssertReturnVoid(!paragraphs.isEmpty());
|
---|
90 |
|
---|
91 | /* Enumerate all the paragraphs: */
|
---|
92 | QStringPairList details;
|
---|
93 | foreach (const QString &strParagraph, paragraphs)
|
---|
94 | {
|
---|
95 | /* Split each paragraph into pairs: */
|
---|
96 | QStringList parts(strParagraph.split("<!--EOM-->", Qt::KeepEmptyParts));
|
---|
97 | /* Make sure each paragraph consist of 2 parts: */
|
---|
98 | AssertReturnVoid(parts.size() == 2);
|
---|
99 | /* Append each pair into details-list: */
|
---|
100 | details << QStringPair(parts[0], parts[1]);
|
---|
101 | }
|
---|
102 |
|
---|
103 | /* Pass details-list to details-container: */
|
---|
104 | m_pDetailsContainer->setDetails(details);
|
---|
105 | /* Update details-container finally: */
|
---|
106 | updateDetailsContainer();
|
---|
107 | }
|
---|
108 |
|
---|
109 | bool QIMessageBox::flagChecked() const
|
---|
110 | {
|
---|
111 | return m_pFlagCheckBox->isChecked();
|
---|
112 | }
|
---|
113 |
|
---|
114 | void QIMessageBox::setFlagChecked(bool fChecked)
|
---|
115 | {
|
---|
116 | m_pFlagCheckBox->setChecked(fChecked);
|
---|
117 | }
|
---|
118 |
|
---|
119 | void QIMessageBox::setFlagText(const QString &strFlagText)
|
---|
120 | {
|
---|
121 | /* Pass text to flag check-box: */
|
---|
122 | m_pFlagCheckBox->setText(strFlagText);
|
---|
123 | /* Update flag check-box finally: */
|
---|
124 | updateCheckBox();
|
---|
125 | }
|
---|
126 |
|
---|
127 | void QIMessageBox::setButtonText(int iButton, const QString &strText)
|
---|
128 | {
|
---|
129 | switch (iButton)
|
---|
130 | {
|
---|
131 | case 0: if (m_pButton1) m_pButton1->setText(strText); break;
|
---|
132 | case 1: if (m_pButton2) m_pButton2->setText(strText); break;
|
---|
133 | case 2: if (m_pButton3) m_pButton3->setText(strText); break;
|
---|
134 | default: break;
|
---|
135 | }
|
---|
136 | }
|
---|
137 |
|
---|
138 | void QIMessageBox::polishEvent(QShowEvent *pPolishEvent)
|
---|
139 | {
|
---|
140 | /* Call to base-class: */
|
---|
141 | QIDialog::polishEvent(pPolishEvent);
|
---|
142 |
|
---|
143 | /* Update size finally: */
|
---|
144 | sltUpdateSize();
|
---|
145 | }
|
---|
146 |
|
---|
147 | void QIMessageBox::closeEvent(QCloseEvent *pCloseEvent)
|
---|
148 | {
|
---|
149 | if (m_fDone)
|
---|
150 | pCloseEvent->accept();
|
---|
151 | else
|
---|
152 | {
|
---|
153 | pCloseEvent->ignore();
|
---|
154 | reject();
|
---|
155 | }
|
---|
156 | }
|
---|
157 |
|
---|
158 | void QIMessageBox::sltUpdateSize()
|
---|
159 | {
|
---|
160 | /* Fix minimum possible size: */
|
---|
161 | setFixedSize(minimumSizeHint());
|
---|
162 | }
|
---|
163 |
|
---|
164 | void QIMessageBox::sltCopy() const
|
---|
165 | {
|
---|
166 | /* Create the error string with all errors. First the html version. */
|
---|
167 | QString strError = "<html><body><p>" + m_strMessage + "</p>";
|
---|
168 | foreach (const QStringPair &pair, m_pDetailsContainer->details())
|
---|
169 | strError += pair.first + pair.second + "<br>";
|
---|
170 | strError += "</body></html>";
|
---|
171 | strError.remove(QRegularExpression("</+qt>"));
|
---|
172 | strError = strError.replace(QRegularExpression(" "), " ");
|
---|
173 | /* Create a new mime data object holding both the html and the plain text version. */
|
---|
174 | QMimeData *pMimeData = new QMimeData();
|
---|
175 | pMimeData->setHtml(strError);
|
---|
176 | /* Replace all the html entities. */
|
---|
177 | strError = strError.replace(QRegularExpression("<br>|</tr>"), "\n");
|
---|
178 | strError = strError.replace(QRegularExpression("</p>"), "\n\n");
|
---|
179 | strError = strError.remove(QRegularExpression("<[^>]*>"));
|
---|
180 | pMimeData->setText(strError);
|
---|
181 | /* Add the mime data to the global clipboard. */
|
---|
182 | QClipboard *pClipboard = QApplication::clipboard();
|
---|
183 | pClipboard->setMimeData(pMimeData);
|
---|
184 | }
|
---|
185 |
|
---|
186 | void QIMessageBox::reject()
|
---|
187 | {
|
---|
188 | if (m_iButtonEsc)
|
---|
189 | {
|
---|
190 | QDialog::reject();
|
---|
191 | setResult(m_iButtonEsc & AlertButtonMask);
|
---|
192 | }
|
---|
193 | }
|
---|
194 |
|
---|
195 | void QIMessageBox::prepare()
|
---|
196 | {
|
---|
197 | /* Set caption: */
|
---|
198 | setWindowTitle(m_strTitle);
|
---|
199 |
|
---|
200 | /* Create main-layout: */
|
---|
201 | QVBoxLayout *pMainLayout = new QVBoxLayout(this);
|
---|
202 | AssertPtrReturnVoid(pMainLayout);
|
---|
203 | {
|
---|
204 | /* Configure main-layout: */
|
---|
205 | #ifdef VBOX_WS_MAC
|
---|
206 | pMainLayout->setContentsMargins(40, 20, 40, 20);
|
---|
207 | pMainLayout->setSpacing(15);
|
---|
208 | #else
|
---|
209 | pMainLayout->setSpacing(qApp->style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing) * 2);
|
---|
210 | #endif
|
---|
211 | /* Create top-layout: */
|
---|
212 | QHBoxLayout *pTopLayout = new QHBoxLayout;
|
---|
213 | AssertPtrReturnVoid(pTopLayout);
|
---|
214 | {
|
---|
215 | /* Configure top-layout: */
|
---|
216 | pTopLayout->setContentsMargins(0, 0, 0, 0);
|
---|
217 | /* Create icon-label: */
|
---|
218 | m_pLabelIcon = new QLabel;
|
---|
219 | AssertPtrReturnVoid(m_pLabelIcon);
|
---|
220 | {
|
---|
221 | /* Configure icon-label: */
|
---|
222 | m_pLabelIcon->setPixmap(standardPixmap(m_iconType, this));
|
---|
223 | m_pLabelIcon->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
|
---|
224 | m_pLabelIcon->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum);
|
---|
225 | /* Add icon-label into top-layout: */
|
---|
226 | pTopLayout->addWidget(m_pLabelIcon);
|
---|
227 | }
|
---|
228 | /* Create text-label: */
|
---|
229 | m_pLabelText = new QIRichTextLabel;
|
---|
230 | AssertPtrReturnVoid(m_pLabelText);
|
---|
231 | {
|
---|
232 | /* Configure text-label: */
|
---|
233 | m_pLabelText->setText(compressLongWords(m_strMessage));
|
---|
234 | /* Add text-label into top-layout: */
|
---|
235 | pTopLayout->addWidget(m_pLabelText);
|
---|
236 | }
|
---|
237 | /* Add top-layout into main-layout: */
|
---|
238 | pMainLayout->addLayout(pTopLayout);
|
---|
239 | }
|
---|
240 | /* Create details-container: */
|
---|
241 | m_pDetailsContainer = new QIArrowSplitter;
|
---|
242 | AssertPtrReturnVoid(m_pDetailsContainer);
|
---|
243 | {
|
---|
244 | /* Configure container: */
|
---|
245 | connect(m_pDetailsContainer, &QIArrowSplitter::sigSizeHintChange,
|
---|
246 | this, &QIMessageBox::sltUpdateSize);
|
---|
247 | /* Add details-container into main-layout: */
|
---|
248 | pMainLayout->addWidget(m_pDetailsContainer);
|
---|
249 | /* Update details-container finally: */
|
---|
250 | updateDetailsContainer();
|
---|
251 | }
|
---|
252 | /* Create flag check-box: */
|
---|
253 | m_pFlagCheckBox = new QCheckBox;
|
---|
254 | AssertPtrReturnVoid(m_pFlagCheckBox);
|
---|
255 | {
|
---|
256 | /* Configure flag check-box: */
|
---|
257 | m_pFlagCheckBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
|
---|
258 | /* Add flag check-box into main-layout: */
|
---|
259 | pMainLayout->addWidget(m_pFlagCheckBox, 0, Qt::AlignHCenter | Qt::AlignVCenter);
|
---|
260 | /* Update flag check-box finally: */
|
---|
261 | updateCheckBox();
|
---|
262 | }
|
---|
263 | /* Create button-box: */
|
---|
264 | m_pButtonBox = new QIDialogButtonBox;
|
---|
265 | AssertPtrReturnVoid(m_pButtonBox);
|
---|
266 | {
|
---|
267 | /* Configure button-box: */
|
---|
268 | m_pButtonBox->setCenterButtons(true);
|
---|
269 | m_pButton1 = createButton(m_iButton1);
|
---|
270 | if (m_pButton1)
|
---|
271 | connect(m_pButton1, &QPushButton::clicked, this, &QIMessageBox::sltDone1);
|
---|
272 | m_pButton2 = createButton(m_iButton2);
|
---|
273 | if (m_pButton2)
|
---|
274 | connect(m_pButton2, &QPushButton::clicked, this, &QIMessageBox::sltDone2);
|
---|
275 | m_pButton3 = createButton(m_iButton3);
|
---|
276 | if (m_pButton3)
|
---|
277 | connect(m_pButton3, &QPushButton::clicked, this, &QIMessageBox::sltDone3);
|
---|
278 | /* Create the help button and connect it to relevant slot in case a help word is supplied: */
|
---|
279 | if (!m_strHelpKeyword.isEmpty())
|
---|
280 | {
|
---|
281 | m_pButtonHelp = createButton(AlertButton_Help);
|
---|
282 | if (m_pButtonHelp)
|
---|
283 | {
|
---|
284 | uiCommon().setHelpKeyword(m_pButtonHelp, m_strHelpKeyword);
|
---|
285 | connect(m_pButtonHelp, &QPushButton::clicked, m_pButtonBox, &QIDialogButtonBox::sltHandleHelpRequest);
|
---|
286 | }
|
---|
287 | }
|
---|
288 |
|
---|
289 | /* Make sure Escape button always set: */
|
---|
290 | Assert(m_iButtonEsc);
|
---|
291 | /* If this is a critical message add a "Copy to clipboard" button: */
|
---|
292 | if (m_iconType == AlertIconType_Critical)
|
---|
293 | {
|
---|
294 | QPushButton *pCopyButton = createButton(AlertButton_Copy);
|
---|
295 | pCopyButton->setToolTip(tr("Copy all errors to the clipboard"));
|
---|
296 | connect(pCopyButton, &QPushButton::clicked, this, &QIMessageBox::sltCopy);
|
---|
297 | }
|
---|
298 | /* Add button-box into main-layout: */
|
---|
299 | pMainLayout->addWidget(m_pButtonBox);
|
---|
300 |
|
---|
301 | /* Prepare focus. It is important to prepare focus after adding button-box to the layout as
|
---|
302 | * parenting the button-box to the QDialog changes default button focus by Qt: */
|
---|
303 | prepareFocus();
|
---|
304 | }
|
---|
305 | }
|
---|
306 | }
|
---|
307 |
|
---|
308 | void QIMessageBox::prepareFocus()
|
---|
309 | {
|
---|
310 | /* Configure default button and focus: */
|
---|
311 | if (m_pButton1 && (m_iButton1 & AlertButtonOption_Default))
|
---|
312 | {
|
---|
313 | m_pButton1->setDefault(true);
|
---|
314 | m_pButton1->setFocus();
|
---|
315 | }
|
---|
316 | if (m_pButton2 && (m_iButton2 & AlertButtonOption_Default))
|
---|
317 | {
|
---|
318 | m_pButton2->setDefault(true);
|
---|
319 | m_pButton2->setFocus();
|
---|
320 | }
|
---|
321 | if (m_pButton3 && (m_iButton3 & AlertButtonOption_Default))
|
---|
322 | {
|
---|
323 | m_pButton3->setDefault(true);
|
---|
324 | m_pButton3->setFocus();
|
---|
325 | }
|
---|
326 | }
|
---|
327 |
|
---|
328 | QPushButton *QIMessageBox::createButton(int iButton)
|
---|
329 | {
|
---|
330 | /* Not for AlertButton_NoButton: */
|
---|
331 | if (iButton == 0)
|
---|
332 | return 0;
|
---|
333 |
|
---|
334 | /* Prepare button text & role: */
|
---|
335 | QString strText;
|
---|
336 | QDialogButtonBox::ButtonRole role;
|
---|
337 | switch (iButton & AlertButtonMask)
|
---|
338 | {
|
---|
339 | case AlertButton_Ok: strText = tr("OK"); role = QDialogButtonBox::AcceptRole; break;
|
---|
340 | case AlertButton_Cancel: strText = tr("Cancel"); role = QDialogButtonBox::RejectRole; break;
|
---|
341 | case AlertButton_Choice1: strText = tr("Yes"); role = QDialogButtonBox::YesRole; break;
|
---|
342 | case AlertButton_Choice2: strText = tr("No"); role = QDialogButtonBox::NoRole; break;
|
---|
343 | case AlertButton_Copy: strText = tr("Copy"); role = QDialogButtonBox::ActionRole; break;
|
---|
344 | case AlertButton_Help: strText = tr("Help"); role = QDialogButtonBox::HelpRole; break;
|
---|
345 | default:
|
---|
346 | AssertMsgFailed(("Type %d is not supported!", iButton));
|
---|
347 | return 0;
|
---|
348 | }
|
---|
349 |
|
---|
350 | /* Create push-button: */
|
---|
351 | QPushButton *pButton = m_pButtonBox->addButton(strText, role);
|
---|
352 |
|
---|
353 | /* Configure <escape> button: */
|
---|
354 | if (iButton & AlertButtonOption_Escape)
|
---|
355 | m_iButtonEsc = iButton & AlertButtonMask;
|
---|
356 |
|
---|
357 | /* Return button: */
|
---|
358 | return pButton;
|
---|
359 | }
|
---|
360 |
|
---|
361 | void QIMessageBox::updateDetailsContainer()
|
---|
362 | {
|
---|
363 | /* Details-container with details is always visible: */
|
---|
364 | m_pDetailsContainer->setVisible(!m_pDetailsContainer->details().isEmpty());
|
---|
365 | /* Update size: */
|
---|
366 | sltUpdateSize();
|
---|
367 | }
|
---|
368 |
|
---|
369 | void QIMessageBox::updateCheckBox()
|
---|
370 | {
|
---|
371 | /* Flag check-box with text is always visible: */
|
---|
372 | m_pFlagCheckBox->setVisible(!m_pFlagCheckBox->text().isEmpty());
|
---|
373 | /* Update size: */
|
---|
374 | sltUpdateSize();
|
---|
375 | }
|
---|
376 |
|
---|
377 | /* static */
|
---|
378 | QPixmap QIMessageBox::standardPixmap(AlertIconType iconType, QWidget *pWidget /* = 0*/)
|
---|
379 | {
|
---|
380 | /* Prepare standard icon: */
|
---|
381 | QIcon icon;
|
---|
382 | switch (iconType)
|
---|
383 | {
|
---|
384 | case AlertIconType_Information: icon = UIIconPool::defaultIcon(UIIconPool::UIDefaultIconType_MessageBoxInformation, pWidget); break;
|
---|
385 | case AlertIconType_Warning: icon = UIIconPool::defaultIcon(UIIconPool::UIDefaultIconType_MessageBoxWarning, pWidget); break;
|
---|
386 | case AlertIconType_Critical: icon = UIIconPool::defaultIcon(UIIconPool::UIDefaultIconType_MessageBoxCritical, pWidget); break;
|
---|
387 | case AlertIconType_Question: icon = UIIconPool::defaultIcon(UIIconPool::UIDefaultIconType_MessageBoxQuestion, pWidget); break;
|
---|
388 | case AlertIconType_GuruMeditation: icon = UIIconPool::iconSet(":/meditation_32px.png"); break;
|
---|
389 | default: break;
|
---|
390 | }
|
---|
391 | /* Return empty pixmap if nothing found: */
|
---|
392 | if (icon.isNull())
|
---|
393 | return QPixmap();
|
---|
394 | /* Return pixmap of standard size if possible: */
|
---|
395 | QStyle *pStyle = pWidget ? pWidget->style() : QApplication::style();
|
---|
396 | int iSize = pStyle->pixelMetric(QStyle::PM_MessageBoxIconSize, 0, pWidget);
|
---|
397 | return icon.pixmap(iSize, iSize);
|
---|
398 | }
|
---|
399 |
|
---|
400 | /* static */
|
---|
401 | QString QIMessageBox::compressLongWords(QString strText)
|
---|
402 | {
|
---|
403 | // WORKAROUND:
|
---|
404 | // The idea is to compress long words of more than 100 symbols in size consisting of alphanumeric
|
---|
405 | // characters with ellipsiss using the following template:
|
---|
406 | // "[50 first symbols]...[50 last symbols]"
|
---|
407 | const QRegularExpression re("[a-zA-Z0-9]{101,}");
|
---|
408 | QRegularExpressionMatch mt = re.match(strText);
|
---|
409 | int iPosition = mt.capturedStart();
|
---|
410 | bool fChangeAllowed = iPosition != -1;
|
---|
411 | while (fChangeAllowed)
|
---|
412 | {
|
---|
413 | QString strNewText = strText;
|
---|
414 | const QString strFound = mt.captured();
|
---|
415 | strNewText.replace(iPosition, strFound.size(), strFound.left(50) + "..." + strFound.right(50));
|
---|
416 | fChangeAllowed = fChangeAllowed && strText != strNewText;
|
---|
417 | strText = strNewText;
|
---|
418 | mt = re.match(strText);
|
---|
419 | iPosition = mt.capturedStart();
|
---|
420 | fChangeAllowed = fChangeAllowed && iPosition != -1;
|
---|
421 | }
|
---|
422 | return strText;
|
---|
423 | }
|
---|