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"