1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2020 Ahmad Samir <a.samirh78@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "kmessagedialog.h"
9#include "kmessagebox_p.h"
10
11#include "loggingcategory.h"
12
13#include <QApplication>
14#include <QCheckBox>
15#include <QDebug>
16#include <QDialogButtonBox>
17#include <QHBoxLayout>
18#include <QLabel>
19#include <QListWidget>
20#include <QPushButton>
21#include <QScreen>
22#include <QScrollArea>
23#include <QScrollBar>
24#include <QShowEvent>
25#include <QStyle>
26#include <QStyleOption>
27#include <QTextBrowser>
28#include <QVBoxLayout>
29#include <QWindow>
30
31#include <KCollapsibleGroupBox>
32#include <KSqueezedTextLabel>
33
34static const Qt::TextInteractionFlags s_textFlags = Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard;
35
36// TODO KF6 remove QObject inheritance again
37class KMessageDialogPrivate : public QObject
38{
39 Q_OBJECT
40
41public:
42 explicit KMessageDialogPrivate(KMessageDialog::Type type, KMessageDialog *qq)
43 : m_type(type)
44 , q(qq)
45 {
46 }
47
48 KMessageDialog::Type m_type;
49 KMessageDialog *const q;
50
51 QVBoxLayout *m_topLayout = nullptr;
52 QWidget *m_mainWidget = nullptr;
53 QLabel *m_iconLabel = nullptr;
54 QLabel *m_messageLabel = nullptr;
55 QListWidget *m_listWidget = nullptr;
56 QLabel *m_detailsLabel = nullptr;
57 QTextBrowser *m_detailsTextEdit = nullptr;
58 KCollapsibleGroupBox *m_detailsGroup = nullptr;
59 QCheckBox *m_dontAskAgainCB = nullptr;
60 QDialogButtonBox *m_buttonBox = nullptr;
61 QMetaObject::Connection m_buttonBoxConnection;
62 bool m_notifyEnabled = true;
63};
64
65KMessageDialog::KMessageDialog(KMessageDialog::Type type, const QString &text, QWidget *parent)
66 : QDialog(parent)
67 , d(new KMessageDialogPrivate(type, this))
68{
69 // Dialog top-level layout
70 d->m_topLayout = new QVBoxLayout(this);
71 d->m_topLayout->setSizeConstraint(QLayout::SetFixedSize);
72
73 // Main widget
74 d->m_mainWidget = new QWidget(this);
75 d->m_topLayout->addWidget(d->m_mainWidget);
76
77 // Layout for the main widget
78 auto *mainLayout = new QVBoxLayout(d->m_mainWidget);
79 QStyle *widgetStyle = d->m_mainWidget->style();
80 // Provide extra spacing
81 mainLayout->setSpacing(widgetStyle->pixelMetric(metric: QStyle::PM_LayoutVerticalSpacing) * 2);
82 mainLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
83
84 auto *hLayout = new QHBoxLayout{};
85 mainLayout->addLayout(layout: hLayout, stretch: 5);
86
87 // Icon
88 auto *iconLayout = new QVBoxLayout{};
89 hLayout->addLayout(layout: iconLayout, stretch: 0);
90
91 d->m_iconLabel = new QLabel(d->m_mainWidget);
92 d->m_iconLabel->setVisible(false);
93 iconLayout->addWidget(d->m_iconLabel);
94 hLayout->addSpacing(size: widgetStyle->pixelMetric(metric: QStyle::PM_LayoutHorizontalSpacing));
95
96 const QRect desktop = screen()->geometry();
97 const auto desktopWidth = desktop.width();
98 // Main message text
99 d->m_messageLabel = new QLabel(text, d->m_mainWidget);
100 if (d->m_messageLabel->sizeHint().width() > (desktopWidth * 0.5)) {
101 // Enable automatic wrapping of messages which are longer than 50% of screen width
102 d->m_messageLabel->setWordWrap(true);
103 // Use a squeezed label if text is still too wide
104 const bool usingSqueezedLabel = d->m_messageLabel->sizeHint().width() > (desktopWidth * 0.85);
105 if (usingSqueezedLabel) {
106 delete d->m_messageLabel;
107 d->m_messageLabel = new KSqueezedTextLabel(text, d->m_mainWidget);
108 }
109 }
110
111 d->m_messageLabel->setTextInteractionFlags(s_textFlags);
112
113 const bool usingScrollArea = (desktop.height() / 3) < d->m_messageLabel->sizeHint().height();
114 if (usingScrollArea) {
115 QScrollArea *messageScrollArea = new QScrollArea(d->m_mainWidget);
116 messageScrollArea->setWidget(d->m_messageLabel);
117 messageScrollArea->setFrameShape(QFrame::NoFrame);
118 messageScrollArea->setWidgetResizable(true);
119 hLayout->addWidget(messageScrollArea, stretch: 5);
120 } else {
121 hLayout->addWidget(d->m_messageLabel, stretch: 5);
122 }
123
124 // List widget, will be populated by setListWidgetItems()
125 d->m_listWidget = new QListWidget(d->m_mainWidget);
126 mainLayout->addWidget(d->m_listWidget, stretch: usingScrollArea ? 10 : 50);
127 d->m_listWidget->setVisible(false);
128
129 // DontAskAgain checkbox, will be set up by setDontAskAgainText()
130 d->m_dontAskAgainCB = new QCheckBox(d->m_mainWidget);
131 mainLayout->addWidget(d->m_dontAskAgainCB);
132 d->m_dontAskAgainCB->setVisible(false);
133
134 // Details widget, text will be added by setDetails()
135 auto *detailsHLayout = new QHBoxLayout{};
136 d->m_topLayout->addLayout(layout: detailsHLayout);
137
138 d->m_detailsGroup = new KCollapsibleGroupBox();
139 d->m_detailsGroup->setVisible(false);
140 d->m_detailsGroup->setTitle(QApplication::translate(context: "KMessageDialog", key: "Details"));
141 QVBoxLayout *detailsLayout = new QVBoxLayout(d->m_detailsGroup);
142
143 d->m_detailsLabel = new QLabel();
144 d->m_detailsLabel->setTextInteractionFlags(s_textFlags);
145 d->m_detailsLabel->setWordWrap(true);
146 detailsLayout->addWidget(d->m_detailsLabel);
147
148 d->m_detailsTextEdit = new QTextBrowser{};
149 d->m_detailsTextEdit->setMinimumHeight(d->m_detailsTextEdit->fontMetrics().lineSpacing() * 11);
150 detailsLayout->addWidget(d->m_detailsTextEdit, stretch: 50);
151
152 detailsHLayout->addWidget(d->m_detailsGroup);
153
154 // Button box
155 d->m_buttonBox = new QDialogButtonBox(this);
156 d->m_topLayout->addWidget(d->m_buttonBox);
157
158 // Default buttons
159 if ((d->m_type == KMessageDialog::Information) || (d->m_type != KMessageDialog::Error)) {
160 // set Ok button
161 setButtons();
162 } else if (d->m_type == KMessageDialog::WarningContinueCancel) {
163 // set Continue & Cancel buttons
164 setButtons(primaryAction: KStandardGuiItem::cont(), secondaryAction: KGuiItem(), cancelAction: KStandardGuiItem::cancel());
165 }
166
167 setNotifyEnabled(true);
168
169 // If the dialog is rejected, e.g. by pressing Esc, done() signal connected to the button box
170 // won't be emitted
171 connect(sender: this, signal: &QDialog::rejected, context: this, slot: [this]() {
172 done(KMessageDialog::Cancel);
173 });
174}
175
176// This method has been copied from KWindowSystem to avoid depending on it
177static void setMainWindow(QDialog *dialog, WId mainWindowId)
178{
179#ifdef Q_OS_OSX
180 if (!QWidget::find(mainWindowId)) {
181 return;
182 }
183#endif
184 // Set the WA_NativeWindow attribute to force the creation of the QWindow.
185 // Without this QWidget::windowHandle() returns 0.
186 dialog->setAttribute(Qt::WA_NativeWindow, on: true);
187 QWindow *subWindow = dialog->windowHandle();
188 Q_ASSERT(subWindow);
189
190 QWindow *mainWindow = QWindow::fromWinId(id: mainWindowId);
191 if (!mainWindow) {
192 // foreign windows not supported on all platforms
193 return;
194 }
195 // mainWindow is not the child of any object, so make sure it gets deleted at some point
196 QObject::connect(sender: dialog, signal: &QObject::destroyed, context: mainWindow, slot: &QObject::deleteLater);
197 subWindow->setTransientParent(mainWindow);
198}
199
200KMessageDialog::KMessageDialog(KMessageDialog::Type type, const QString &text, WId parent_id)
201 : KMessageDialog(type, text)
202{
203 QWidget *parent = QWidget::find(parent_id);
204 setParent(parent);
205 if (!parent && parent_id) {
206 setMainWindow(dialog: this, mainWindowId: parent_id);
207 }
208}
209
210KMessageDialog::~KMessageDialog()
211{
212 removeEventFilter(obj: d.get());
213}
214
215void KMessageDialog::setCaption(const QString &caption)
216{
217 if (!caption.isEmpty()) {
218 setWindowTitle(caption);
219 return;
220 }
221
222 QString title;
223 switch (d->m_type) { // Get a title based on the dialog Type
224 case KMessageDialog::QuestionTwoActions:
225 case KMessageDialog::QuestionTwoActionsCancel:
226 title = QApplication::translate(context: "KMessageDialog", key: "Question");
227 break;
228 case KMessageDialog::WarningTwoActions:
229 case KMessageDialog::WarningTwoActionsCancel:
230 case KMessageDialog::WarningContinueCancel:
231 title = QApplication::translate(context: "KMessageDialog", key: "Warning");
232 break;
233 case KMessageDialog::Information:
234 title = QApplication::translate(context: "KMessageDialog", key: "Information");
235 break;
236 case KMessageDialog::Error: {
237 title = QApplication::translate(context: "KMessageDialog", key: "Error");
238 break;
239 }
240 default:
241 break;
242 }
243
244 setWindowTitle(title);
245}
246
247void KMessageDialog::setIcon(const QIcon &icon)
248{
249 QIcon effectiveIcon(icon);
250 if (effectiveIcon.isNull()) { // Fallback to an icon based on the dialog Type
251 QStyle *style = this->style();
252 switch (d->m_type) {
253 case KMessageDialog::QuestionTwoActions:
254 case KMessageDialog::QuestionTwoActionsCancel:
255 effectiveIcon = style->standardIcon(standardIcon: QStyle::SP_MessageBoxQuestion, option: nullptr, widget: this);
256 break;
257 case KMessageDialog::WarningTwoActions:
258 case KMessageDialog::WarningTwoActionsCancel:
259 case KMessageDialog::WarningContinueCancel:
260 effectiveIcon = style->standardIcon(standardIcon: QStyle::SP_MessageBoxWarning, option: nullptr, widget: this);
261 break;
262 case KMessageDialog::Information:
263 effectiveIcon = style->standardIcon(standardIcon: QStyle::SP_MessageBoxInformation, option: nullptr, widget: this);
264 break;
265 case KMessageDialog::Error:
266 effectiveIcon = style->standardIcon(standardIcon: QStyle::SP_MessageBoxCritical, option: nullptr, widget: this);
267 break;
268 default:
269 break;
270 }
271 }
272
273 if (effectiveIcon.isNull()) {
274 qCWarning(KWidgetsAddonsLog) << "Neither the requested icon nor a generic one based on the "
275 "dialog type could be found.";
276 return;
277 }
278
279 d->m_iconLabel->setVisible(true);
280
281 QStyleOption option;
282 option.initFrom(w: d->m_mainWidget);
283 QStyle *widgetStyle = d->m_mainWidget->style();
284 const int size = widgetStyle->pixelMetric(metric: QStyle::PM_MessageBoxIconSize, option: &option, widget: d->m_mainWidget);
285 d->m_iconLabel->setPixmap(effectiveIcon.pixmap(extent: size));
286}
287
288void KMessageDialog::setListWidgetItems(const QStringList &strlist)
289{
290 const bool isEmpty = strlist.isEmpty();
291 d->m_listWidget->setVisible(!isEmpty);
292 if (isEmpty) {
293 return;
294 }
295
296 // Enable automatic wrapping since the listwidget already has a good initial width
297 d->m_messageLabel->setWordWrap(true);
298 d->m_listWidget->addItems(labels: strlist);
299
300 QStyleOptionViewItem styleOption;
301 styleOption.initFrom(w: d->m_listWidget);
302 QFontMetrics fm(styleOption.font);
303 int listWidth = d->m_listWidget->width();
304 for (const QString &str : strlist) {
305 listWidth = qMax(a: listWidth, b: fm.boundingRect(text: str).width());
306 }
307 const int borderWidth = (d->m_listWidget->width() - d->m_listWidget->viewport()->width() //
308 + d->m_listWidget->verticalScrollBar()->height());
309 listWidth += borderWidth;
310 const auto deskWidthPortion = screen()->geometry().width() * 0.85;
311 if (listWidth > deskWidthPortion) { // Limit the list widget size to 85% of screen width
312 listWidth = qRound(d: deskWidthPortion);
313 }
314 d->m_listWidget->setMinimumWidth(listWidth);
315 d->m_listWidget->setSelectionMode(QListWidget::NoSelection);
316 d->m_messageLabel->setSizePolicy(hor: QSizePolicy::Preferred, ver: QSizePolicy::Minimum);
317}
318
319void KMessageDialog::setDetails(const QString &details)
320{
321 d->m_detailsGroup->setVisible(!details.isEmpty());
322
323 if (details.length() < 512) { // random number KMessageBox uses.
324 d->m_detailsLabel->setText(details);
325 d->m_detailsLabel->show();
326
327 d->m_detailsTextEdit->setText(QString());
328 d->m_detailsTextEdit->hide();
329 } else {
330 d->m_detailsLabel->setText(QString());
331 d->m_detailsLabel->hide();
332
333 d->m_detailsTextEdit->setText(details);
334 d->m_detailsTextEdit->show();
335 }
336}
337
338void KMessageDialog::setButtons(const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const KGuiItem &cancelAction)
339{
340 switch (d->m_type) {
341 case KMessageDialog::QuestionTwoActions: {
342 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No);
343 auto *buttonYes = d->m_buttonBox->button(which: QDialogButtonBox::Yes);
344 KGuiItem::assign(button: buttonYes, item: primaryAction);
345 buttonYes->setFocus();
346 KGuiItem::assign(button: d->m_buttonBox->button(which: QDialogButtonBox::No), item: secondaryAction);
347 break;
348 }
349 case KMessageDialog::QuestionTwoActionsCancel: {
350 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel);
351 auto *buttonYes = d->m_buttonBox->button(which: QDialogButtonBox::Yes);
352 KGuiItem::assign(button: buttonYes, item: primaryAction);
353 buttonYes->setFocus();
354 KGuiItem::assign(button: d->m_buttonBox->button(which: QDialogButtonBox::No), item: secondaryAction);
355 KGuiItem::assign(button: d->m_buttonBox->button(which: QDialogButtonBox::Cancel), item: cancelAction);
356 break;
357 }
358 case KMessageDialog::WarningTwoActions: {
359 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No);
360 KGuiItem::assign(button: d->m_buttonBox->button(which: QDialogButtonBox::Yes), item: primaryAction);
361
362 auto *noBtn = d->m_buttonBox->button(which: QDialogButtonBox::No);
363 KGuiItem::assign(button: noBtn, item: secondaryAction);
364 noBtn->setDefault(true);
365 noBtn->setFocus();
366 break;
367 }
368 case KMessageDialog::WarningTwoActionsCancel: {
369 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel);
370 KGuiItem::assign(button: d->m_buttonBox->button(which: QDialogButtonBox::Yes), item: primaryAction);
371 KGuiItem::assign(button: d->m_buttonBox->button(which: QDialogButtonBox::No), item: secondaryAction);
372
373 auto *cancelButton = d->m_buttonBox->button(which: QDialogButtonBox::Cancel);
374 KGuiItem::assign(button: cancelButton, item: cancelAction);
375 cancelButton->setDefault(true);
376 cancelButton->setFocus();
377 break;
378 }
379 case KMessageDialog::WarningContinueCancel: {
380 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::Cancel);
381
382 KGuiItem::assign(button: d->m_buttonBox->button(which: QDialogButtonBox::Yes), item: primaryAction);
383
384 auto *cancelButton = d->m_buttonBox->button(which: QDialogButtonBox::Cancel);
385 KGuiItem::assign(button: cancelButton, item: cancelAction);
386 cancelButton->setDefault(true);
387 cancelButton->setFocus();
388 break;
389 }
390 case KMessageDialog::Information:
391 case KMessageDialog::Error: {
392 d->m_buttonBox->setStandardButtons(QDialogButtonBox::Ok);
393 auto *okButton = d->m_buttonBox->button(which: QDialogButtonBox::Ok);
394 KGuiItem::assign(button: okButton, item: KStandardGuiItem::ok());
395 okButton->setFocus();
396 break;
397 }
398 default:
399 break;
400 }
401
402 // Button connections
403 if (!d->m_buttonBoxConnection) {
404 d->m_buttonBoxConnection = connect(sender: d->m_buttonBox, signal: &QDialogButtonBox::clicked, context: this, slot: [this](QAbstractButton *button) {
405 QDialogButtonBox::StandardButton code = d->m_buttonBox->standardButton(button);
406 const int result = (code == QDialogButtonBox::Ok) ? KMessageDialog::Ok
407 : (code == QDialogButtonBox::Cancel) ? KMessageDialog::Cancel
408 : (code == QDialogButtonBox::Yes) ? KMessageDialog::PrimaryAction
409 : (code == QDialogButtonBox::No) ? KMessageDialog::SecondaryAction
410 :
411 /* else */ -1;
412 if (result != -1) {
413 done(result);
414 }
415 });
416 }
417}
418
419void KMessageDialog::setDontAskAgainText(const QString &dontAskAgainText)
420{
421 d->m_dontAskAgainCB->setVisible(!dontAskAgainText.isEmpty());
422 d->m_dontAskAgainCB->setText(dontAskAgainText);
423}
424
425void KMessageDialog::setDontAskAgainChecked(bool isChecked)
426{
427 if (d->m_dontAskAgainCB->text().isEmpty()) {
428 qCWarning(KWidgetsAddonsLog) << "setDontAskAgainChecked() method was called on a dialog that doesn't "
429 "appear to have a checkbox; you need to use setDontAskAgainText() "
430 "to add a checkbox to the dialog first.";
431 return;
432 }
433
434 d->m_dontAskAgainCB->setChecked(isChecked);
435}
436
437bool KMessageDialog::isDontAskAgainChecked() const
438{
439 if (d->m_dontAskAgainCB->text().isEmpty()) {
440 qCWarning(KWidgetsAddonsLog) << "isDontAskAgainChecked() method was called on a dialog that doesn't "
441 "appear to have a checkbox; you need to use setDontAskAgainText() "
442 "to add a checkbox to the dialog first.";
443 return false;
444 }
445
446 return d->m_dontAskAgainCB->isChecked();
447}
448
449void KMessageDialog::setOpenExternalLinks(bool isAllowed)
450{
451 d->m_messageLabel->setOpenExternalLinks(isAllowed);
452 d->m_detailsLabel->setOpenExternalLinks(isAllowed);
453 d->m_detailsTextEdit->setOpenExternalLinks(isAllowed);
454}
455
456bool KMessageDialog::isNotifyEnabled() const
457{
458 return d->m_notifyEnabled;
459}
460
461void KMessageDialog::setNotifyEnabled(bool enable)
462{
463 d->m_notifyEnabled = enable;
464}
465
466void KMessageDialog::showEvent(QShowEvent *event)
467{
468 if (d->m_notifyEnabled && !event->spontaneous()) {
469 // TODO include m_listWidget items
470 beep(type: d->m_type, text: d->m_messageLabel->text(), dialog: topLevelWidget());
471 }
472 QDialog::showEvent(event);
473}
474
475void KMessageDialog::beep(Type type, const QString &text, QWidget *widget)
476{
477#ifndef Q_OS_WIN // FIXME problems with KNotify on Windows
478 QMessageBox::Icon notifyType = QMessageBox::NoIcon;
479 switch (type) {
480 case KMessageDialog::QuestionTwoActions:
481 case KMessageDialog::QuestionTwoActionsCancel:
482 notifyType = QMessageBox::Question;
483 break;
484 case KMessageDialog::WarningTwoActions:
485 case KMessageDialog::WarningTwoActionsCancel:
486 case KMessageDialog::WarningContinueCancel:
487 notifyType = QMessageBox::Warning;
488 break;
489 case KMessageDialog::Information:
490 notifyType = QMessageBox::Information;
491 break;
492 case KMessageDialog::Error:
493 notifyType = QMessageBox::Critical;
494 break;
495 }
496
497 KMessageBox::notifyInterface()->sendNotification(notificationType: notifyType, message: text, parent: widget);
498#endif
499}
500
501#include "kmessagedialog.moc"
502#include "moc_kmessagedialog.cpp"
503

source code of kwidgetsaddons/src/kmessagedialog.cpp