File indexing completed on 2024-04-28 03:59:08
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2020 Ahmad Samir <a.samirh78@gmail.com> 0004 0005 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0006 */ 0007 0008 #include "kmessagedialog.h" 0009 #include "kmessagebox_p.h" 0010 0011 #include "loggingcategory.h" 0012 0013 #include <QApplication> 0014 #include <QCheckBox> 0015 #include <QDebug> 0016 #include <QDialogButtonBox> 0017 #include <QHBoxLayout> 0018 #include <QLabel> 0019 #include <QListWidget> 0020 #include <QPushButton> 0021 #include <QScreen> 0022 #include <QScrollArea> 0023 #include <QScrollBar> 0024 #include <QStyle> 0025 #include <QStyleOption> 0026 #include <QTextBrowser> 0027 #include <QVBoxLayout> 0028 #include <QWindow> 0029 0030 #include <KCollapsibleGroupBox> 0031 #include <KSqueezedTextLabel> 0032 0033 static const Qt::TextInteractionFlags s_textFlags = Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard; 0034 0035 // TODO KF6 remove QObject inheritance again 0036 class KMessageDialogPrivate : public QObject 0037 { 0038 Q_OBJECT 0039 0040 public: 0041 explicit KMessageDialogPrivate(KMessageDialog::Type type, KMessageDialog *qq) 0042 : m_type(type) 0043 , q(qq) 0044 { 0045 } 0046 0047 KMessageDialog::Type m_type; 0048 KMessageDialog *const q; 0049 0050 QVBoxLayout *m_topLayout = nullptr; 0051 QWidget *m_mainWidget = nullptr; 0052 QLabel *m_iconLabel = nullptr; 0053 QLabel *m_messageLabel = nullptr; 0054 QListWidget *m_listWidget = nullptr; 0055 QLabel *m_detailsLabel = nullptr; 0056 QTextBrowser *m_detailsTextEdit = nullptr; 0057 KCollapsibleGroupBox *m_detailsGroup = nullptr; 0058 QCheckBox *m_dontAskAgainCB = nullptr; 0059 QDialogButtonBox *m_buttonBox = nullptr; 0060 QMetaObject::Connection m_buttonBoxConnection; 0061 bool m_notifyEnabled = true; 0062 }; 0063 0064 KMessageDialog::KMessageDialog(KMessageDialog::Type type, const QString &text, QWidget *parent) 0065 : QDialog(parent) 0066 , d(new KMessageDialogPrivate(type, this)) 0067 { 0068 // Dialog top-level layout 0069 d->m_topLayout = new QVBoxLayout(this); 0070 d->m_topLayout->setSizeConstraint(QLayout::SetFixedSize); 0071 0072 // Main widget 0073 d->m_mainWidget = new QWidget(this); 0074 d->m_topLayout->addWidget(d->m_mainWidget); 0075 0076 // Layout for the main widget 0077 auto *mainLayout = new QVBoxLayout(d->m_mainWidget); 0078 QStyle *widgetStyle = d->m_mainWidget->style(); 0079 // Provide extra spacing 0080 mainLayout->setSpacing(widgetStyle->pixelMetric(QStyle::PM_LayoutVerticalSpacing) * 2); 0081 mainLayout->setContentsMargins(0, 0, 0, 0); 0082 0083 auto *hLayout = new QHBoxLayout{}; 0084 mainLayout->addLayout(hLayout, 5); 0085 0086 // Icon 0087 auto *iconLayout = new QVBoxLayout{}; 0088 hLayout->addLayout(iconLayout, 0); 0089 0090 d->m_iconLabel = new QLabel(d->m_mainWidget); 0091 d->m_iconLabel->setVisible(false); 0092 iconLayout->addWidget(d->m_iconLabel); 0093 hLayout->addSpacing(widgetStyle->pixelMetric(QStyle::PM_LayoutHorizontalSpacing)); 0094 0095 const QRect desktop = screen()->geometry(); 0096 const auto desktopWidth = desktop.width(); 0097 // Main message text 0098 d->m_messageLabel = new QLabel(text, d->m_mainWidget); 0099 if (d->m_messageLabel->sizeHint().width() > (desktopWidth * 0.5)) { 0100 // Enable automatic wrapping of messages which are longer than 50% of screen width 0101 d->m_messageLabel->setWordWrap(true); 0102 // Use a squeezed label if text is still too wide 0103 const bool usingSqueezedLabel = d->m_messageLabel->sizeHint().width() > (desktopWidth * 0.85); 0104 if (usingSqueezedLabel) { 0105 delete d->m_messageLabel; 0106 d->m_messageLabel = new KSqueezedTextLabel(text, d->m_mainWidget); 0107 } 0108 } 0109 0110 d->m_messageLabel->setTextInteractionFlags(s_textFlags); 0111 0112 const bool usingScrollArea = (desktop.height() / 3) < d->m_messageLabel->sizeHint().height(); 0113 if (usingScrollArea) { 0114 QScrollArea *messageScrollArea = new QScrollArea(d->m_mainWidget); 0115 messageScrollArea->setWidget(d->m_messageLabel); 0116 messageScrollArea->setFrameShape(QFrame::NoFrame); 0117 messageScrollArea->setWidgetResizable(true); 0118 hLayout->addWidget(messageScrollArea, 5); 0119 } else { 0120 hLayout->addWidget(d->m_messageLabel, 5); 0121 } 0122 0123 // List widget, will be populated by setListWidgetItems() 0124 d->m_listWidget = new QListWidget(d->m_mainWidget); 0125 mainLayout->addWidget(d->m_listWidget, usingScrollArea ? 10 : 50); 0126 d->m_listWidget->setVisible(false); 0127 0128 // DontAskAgain checkbox, will be set up by setDontAskAgainText() 0129 d->m_dontAskAgainCB = new QCheckBox(d->m_mainWidget); 0130 mainLayout->addWidget(d->m_dontAskAgainCB); 0131 d->m_dontAskAgainCB->setVisible(false); 0132 0133 // Details widget, text will be added by setDetails() 0134 auto *detailsHLayout = new QHBoxLayout{}; 0135 d->m_topLayout->addLayout(detailsHLayout); 0136 0137 d->m_detailsGroup = new KCollapsibleGroupBox(); 0138 d->m_detailsGroup->setVisible(false); 0139 d->m_detailsGroup->setTitle(QApplication::translate("KMessageDialog", "Details")); 0140 QVBoxLayout *detailsLayout = new QVBoxLayout(d->m_detailsGroup); 0141 0142 d->m_detailsLabel = new QLabel(); 0143 d->m_detailsLabel->setTextInteractionFlags(s_textFlags); 0144 d->m_detailsLabel->setWordWrap(true); 0145 detailsLayout->addWidget(d->m_detailsLabel); 0146 0147 d->m_detailsTextEdit = new QTextBrowser{}; 0148 d->m_detailsTextEdit->setMinimumHeight(d->m_detailsTextEdit->fontMetrics().lineSpacing() * 11); 0149 detailsLayout->addWidget(d->m_detailsTextEdit, 50); 0150 0151 detailsHLayout->addWidget(d->m_detailsGroup); 0152 0153 // Button box 0154 d->m_buttonBox = new QDialogButtonBox(this); 0155 d->m_topLayout->addWidget(d->m_buttonBox); 0156 0157 // Default buttons 0158 if ((d->m_type == KMessageDialog::Information) || (d->m_type != KMessageDialog::Error)) { 0159 // set Ok button 0160 setButtons(); 0161 } else if ((d->m_type == KMessageDialog::WarningContinueCancel)) { 0162 // set Continue & Cancel buttons 0163 setButtons(KStandardGuiItem::cont(), KGuiItem(), KStandardGuiItem::cancel()); 0164 } 0165 0166 setNotifyEnabled(true); 0167 0168 // If the dialog is rejected, e.g. by pressing Esc, done() signal connected to the button box 0169 // won't be emitted 0170 connect(this, &QDialog::rejected, this, [this]() { 0171 done(KMessageDialog::Cancel); 0172 }); 0173 } 0174 0175 // This method has been copied from KWindowSystem to avoid depending on it 0176 static void setMainWindow(QDialog *dialog, WId mainWindowId) 0177 { 0178 #ifdef Q_OS_OSX 0179 if (!QWidget::find(mainWindowId)) { 0180 return; 0181 } 0182 #endif 0183 // Set the WA_NativeWindow attribute to force the creation of the QWindow. 0184 // Without this QWidget::windowHandle() returns 0. 0185 dialog->setAttribute(Qt::WA_NativeWindow, true); 0186 QWindow *subWindow = dialog->windowHandle(); 0187 Q_ASSERT(subWindow); 0188 0189 QWindow *mainWindow = QWindow::fromWinId(mainWindowId); 0190 if (!mainWindow) { 0191 // foreign windows not supported on all platforms 0192 return; 0193 } 0194 // mainWindow is not the child of any object, so make sure it gets deleted at some point 0195 QObject::connect(dialog, &QObject::destroyed, mainWindow, &QObject::deleteLater); 0196 subWindow->setTransientParent(mainWindow); 0197 } 0198 0199 KMessageDialog::KMessageDialog(KMessageDialog::Type type, const QString &text, WId parent_id) 0200 : KMessageDialog(type, text) 0201 { 0202 QWidget *parent = QWidget::find(parent_id); 0203 setParent(parent); 0204 if (!parent && parent_id) { 0205 setMainWindow(this, parent_id); 0206 } 0207 } 0208 0209 KMessageDialog::~KMessageDialog() 0210 { 0211 removeEventFilter(d.get()); 0212 } 0213 0214 void KMessageDialog::setCaption(const QString &caption) 0215 { 0216 if (!caption.isEmpty()) { 0217 setWindowTitle(caption); 0218 return; 0219 } 0220 0221 QString title; 0222 switch (d->m_type) { // Get a title based on the dialog Type 0223 case KMessageDialog::QuestionTwoActions: 0224 case KMessageDialog::QuestionTwoActionsCancel: 0225 title = QApplication::translate("KMessageDialog", "Question"); 0226 break; 0227 case KMessageDialog::WarningTwoActions: 0228 case KMessageDialog::WarningTwoActionsCancel: 0229 case KMessageDialog::WarningContinueCancel: 0230 title = QApplication::translate("KMessageDialog", "Warning"); 0231 break; 0232 case KMessageDialog::Information: 0233 title = QApplication::translate("KMessageDialog", "Information"); 0234 break; 0235 case KMessageDialog::Error: { 0236 title = QApplication::translate("KMessageDialog", "Error"); 0237 break; 0238 } 0239 default: 0240 break; 0241 } 0242 0243 setWindowTitle(title); 0244 } 0245 0246 void KMessageDialog::setIcon(const QIcon &icon) 0247 { 0248 QIcon effectiveIcon(icon); 0249 if (effectiveIcon.isNull()) { // Fallback to an icon based on the dialog Type 0250 QStyle *style = this->style(); 0251 switch (d->m_type) { 0252 case KMessageDialog::QuestionTwoActions: 0253 case KMessageDialog::QuestionTwoActionsCancel: 0254 effectiveIcon = style->standardIcon(QStyle::SP_MessageBoxQuestion, nullptr, this); 0255 break; 0256 case KMessageDialog::WarningTwoActions: 0257 case KMessageDialog::WarningTwoActionsCancel: 0258 case KMessageDialog::WarningContinueCancel: 0259 effectiveIcon = style->standardIcon(QStyle::SP_MessageBoxWarning, nullptr, this); 0260 break; 0261 case KMessageDialog::Information: 0262 effectiveIcon = style->standardIcon(QStyle::SP_MessageBoxInformation, nullptr, this); 0263 break; 0264 case KMessageDialog::Error: 0265 effectiveIcon = style->standardIcon(QStyle::SP_MessageBoxCritical, nullptr, this); 0266 break; 0267 default: 0268 break; 0269 } 0270 } 0271 0272 if (effectiveIcon.isNull()) { 0273 qCWarning(KWidgetsAddonsLog) << "Neither the requested icon nor a generic one based on the " 0274 "dialog type could be found."; 0275 return; 0276 } 0277 0278 d->m_iconLabel->setVisible(true); 0279 0280 QStyleOption option; 0281 option.initFrom(d->m_mainWidget); 0282 QStyle *widgetStyle = d->m_mainWidget->style(); 0283 const int size = widgetStyle->pixelMetric(QStyle::PM_MessageBoxIconSize, &option, d->m_mainWidget); 0284 d->m_iconLabel->setPixmap(effectiveIcon.pixmap(size)); 0285 } 0286 0287 void KMessageDialog::setListWidgetItems(const QStringList &strlist) 0288 { 0289 const bool isEmpty = strlist.isEmpty(); 0290 d->m_listWidget->setVisible(!isEmpty); 0291 if (isEmpty) { 0292 return; 0293 } 0294 0295 // Enable automatic wrapping since the listwidget already has a good initial width 0296 d->m_messageLabel->setWordWrap(true); 0297 d->m_listWidget->addItems(strlist); 0298 0299 QStyleOptionViewItem styleOption; 0300 styleOption.initFrom(d->m_listWidget); 0301 QFontMetrics fm(styleOption.font); 0302 int listWidth = d->m_listWidget->width(); 0303 for (const QString &str : strlist) { 0304 listWidth = qMax(listWidth, fm.boundingRect(str).width()); 0305 } 0306 const int borderWidth = (d->m_listWidget->width() - d->m_listWidget->viewport()->width() // 0307 + d->m_listWidget->verticalScrollBar()->height()); 0308 listWidth += borderWidth; 0309 const auto deskWidthPortion = screen()->geometry().width() * 0.85; 0310 if (listWidth > deskWidthPortion) { // Limit the list widget size to 85% of screen width 0311 listWidth = qRound(deskWidthPortion); 0312 } 0313 d->m_listWidget->setMinimumWidth(listWidth); 0314 d->m_listWidget->setSelectionMode(QListWidget::NoSelection); 0315 d->m_messageLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); 0316 } 0317 0318 void KMessageDialog::setDetails(const QString &details) 0319 { 0320 d->m_detailsGroup->setVisible(!details.isEmpty()); 0321 0322 if (details.length() < 512) { // random number KMessageBox uses. 0323 d->m_detailsLabel->setText(details); 0324 d->m_detailsLabel->show(); 0325 0326 d->m_detailsTextEdit->setText(QString()); 0327 d->m_detailsTextEdit->hide(); 0328 } else { 0329 d->m_detailsLabel->setText(QString()); 0330 d->m_detailsLabel->hide(); 0331 0332 d->m_detailsTextEdit->setText(details); 0333 d->m_detailsTextEdit->show(); 0334 } 0335 } 0336 0337 void KMessageDialog::setButtons(const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const KGuiItem &cancelAction) 0338 { 0339 switch (d->m_type) { 0340 case KMessageDialog::QuestionTwoActions: { 0341 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No); 0342 auto *buttonYes = d->m_buttonBox->button(QDialogButtonBox::Yes); 0343 KGuiItem::assign(buttonYes, primaryAction); 0344 buttonYes->setFocus(); 0345 KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::No), secondaryAction); 0346 break; 0347 } 0348 case KMessageDialog::QuestionTwoActionsCancel: { 0349 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel); 0350 auto *buttonYes = d->m_buttonBox->button(QDialogButtonBox::Yes); 0351 KGuiItem::assign(buttonYes, primaryAction); 0352 buttonYes->setFocus(); 0353 KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::No), secondaryAction); 0354 KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::Cancel), cancelAction); 0355 break; 0356 } 0357 case KMessageDialog::WarningTwoActions: { 0358 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No); 0359 KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::Yes), primaryAction); 0360 0361 auto *noBtn = d->m_buttonBox->button(QDialogButtonBox::No); 0362 KGuiItem::assign(noBtn, secondaryAction); 0363 noBtn->setDefault(true); 0364 noBtn->setFocus(); 0365 break; 0366 } 0367 case KMessageDialog::WarningTwoActionsCancel: { 0368 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel); 0369 KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::Yes), primaryAction); 0370 KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::No), secondaryAction); 0371 0372 auto *cancelButton = d->m_buttonBox->button(QDialogButtonBox::Cancel); 0373 KGuiItem::assign(cancelButton, cancelAction); 0374 cancelButton->setDefault(true); 0375 cancelButton->setFocus(); 0376 break; 0377 } 0378 case KMessageDialog::WarningContinueCancel: { 0379 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::Cancel); 0380 0381 KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::Yes), primaryAction); 0382 0383 auto *cancelButton = d->m_buttonBox->button(QDialogButtonBox::Cancel); 0384 KGuiItem::assign(cancelButton, cancelAction); 0385 cancelButton->setDefault(true); 0386 cancelButton->setFocus(); 0387 break; 0388 } 0389 case KMessageDialog::Information: 0390 case KMessageDialog::Error: { 0391 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Ok); 0392 auto *okButton = d->m_buttonBox->button(QDialogButtonBox::Ok); 0393 KGuiItem::assign(okButton, KStandardGuiItem::ok()); 0394 okButton->setFocus(); 0395 break; 0396 } 0397 default: 0398 break; 0399 } 0400 0401 // Button connections 0402 if (!d->m_buttonBoxConnection) { 0403 d->m_buttonBoxConnection = connect(d->m_buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) { 0404 QDialogButtonBox::StandardButton code = d->m_buttonBox->standardButton(button); 0405 const int result = (code == QDialogButtonBox::Ok) ? KMessageDialog::Ok 0406 : (code == QDialogButtonBox::Cancel) ? KMessageDialog::Cancel 0407 : (code == QDialogButtonBox::Yes) ? KMessageDialog::PrimaryAction 0408 : (code == QDialogButtonBox::No) ? KMessageDialog::SecondaryAction 0409 : 0410 /* else */ -1; 0411 if (result != -1) { 0412 done(result); 0413 } 0414 }); 0415 } 0416 } 0417 0418 void KMessageDialog::setDontAskAgainText(const QString &dontAskAgainText) 0419 { 0420 d->m_dontAskAgainCB->setVisible(!dontAskAgainText.isEmpty()); 0421 d->m_dontAskAgainCB->setText(dontAskAgainText); 0422 } 0423 0424 void KMessageDialog::setDontAskAgainChecked(bool isChecked) 0425 { 0426 if (d->m_dontAskAgainCB->text().isEmpty()) { 0427 qCWarning(KWidgetsAddonsLog) << "setDontAskAgainChecked() method was called on a dialog that doesn't " 0428 "appear to have a checkbox; you need to use setDontAskAgainText() " 0429 "to add a checkbox to the dialog first."; 0430 return; 0431 } 0432 0433 d->m_dontAskAgainCB->setChecked(isChecked); 0434 } 0435 0436 bool KMessageDialog::isDontAskAgainChecked() const 0437 { 0438 if (d->m_dontAskAgainCB->text().isEmpty()) { 0439 qCWarning(KWidgetsAddonsLog) << "isDontAskAgainChecked() method was called on a dialog that doesn't " 0440 "appear to have a checkbox; you need to use setDontAskAgainText() " 0441 "to add a checkbox to the dialog first."; 0442 return false; 0443 } 0444 0445 return d->m_dontAskAgainCB->isChecked(); 0446 } 0447 0448 void KMessageDialog::setOpenExternalLinks(bool isAllowed) 0449 { 0450 d->m_messageLabel->setOpenExternalLinks(isAllowed); 0451 d->m_detailsLabel->setOpenExternalLinks(isAllowed); 0452 d->m_detailsTextEdit->setOpenExternalLinks(isAllowed); 0453 } 0454 0455 bool KMessageDialog::isNotifyEnabled() const 0456 { 0457 return d->m_notifyEnabled; 0458 } 0459 0460 void KMessageDialog::setNotifyEnabled(bool enable) 0461 { 0462 d->m_notifyEnabled = enable; 0463 } 0464 0465 void KMessageDialog::showEvent(QShowEvent *event) 0466 { 0467 if (d->m_notifyEnabled) { 0468 // TODO include m_listWidget items 0469 beep(d->m_type, d->m_messageLabel->text(), topLevelWidget()); 0470 } 0471 QDialog::showEvent(event); 0472 } 0473 0474 void KMessageDialog::beep(Type type, const QString &text, QWidget *widget) 0475 { 0476 #ifndef Q_OS_WIN // FIXME problems with KNotify on Windows 0477 QMessageBox::Icon notifyType = QMessageBox::NoIcon; 0478 switch (type) { 0479 case KMessageDialog::QuestionTwoActions: 0480 case KMessageDialog::QuestionTwoActionsCancel: 0481 notifyType = QMessageBox::Question; 0482 break; 0483 case KMessageDialog::WarningTwoActions: 0484 case KMessageDialog::WarningTwoActionsCancel: 0485 case KMessageDialog::WarningContinueCancel: 0486 notifyType = QMessageBox::Warning; 0487 break; 0488 case KMessageDialog::Information: 0489 notifyType = QMessageBox::Information; 0490 break; 0491 case KMessageDialog::Error: 0492 notifyType = QMessageBox::Critical; 0493 break; 0494 } 0495 0496 KMessageBox::notifyInterface()->sendNotification(notifyType, text, widget); 0497 #endif 0498 } 0499 0500 #include "kmessagedialog.moc" 0501 #include "moc_kmessagedialog.cpp"