File indexing completed on 2024-04-28 15:32:06

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