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

source code of kwidgetsaddons/src/kmessagedialog.cpp