1/*
2 SPDX-FileCopyrightText: 2000 Matthias Elter <elter@kde.org>
3 SPDX-FileCopyrightText: 2003 Daniel Molkentin <molkentin@kde.org>
4 SPDX-FileCopyrightText: 2003, 2006 Matthias Kretz <kretz@kde.org>
5 SPDX-FileCopyrightText: 2004 Frans Englich <frans.englich@telia.com>
6 SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>
7 SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
8
9 SPDX-License-Identifier: LGPL-2.0-or-later
10*/
11
12#include "kcmultidialog.h"
13#include "kcmoduleloader.h"
14#include "kcmoduleqml_p.h"
15#include "kcmultidialog_p.h"
16#include <kcmutils_debug.h>
17
18#include <QApplication>
19#include <QDesktopServices>
20#include <QJsonArray>
21#include <QLayout>
22#include <QProcess>
23#include <QPushButton>
24#include <QScreen>
25#include <QStandardPaths>
26#include <QStringList>
27#include <QStyle>
28#include <QTimer>
29#include <QUrl>
30
31#include <KGuiItem>
32#include <KIconUtils>
33#include <KLocalizedString>
34#include <KMessageBox>
35#include <KPageWidgetModel>
36
37bool KCMultiDialogPrivate::resolveChanges(KCModule *module)
38{
39 if (!module || !module->needsSave()) {
40 return true;
41 }
42
43 // Let the user decide
44 const int queryUser = KMessageBox::warningTwoActionsCancel(q,
45 i18n("The settings of the current module have changed.\n"
46 "Do you want to apply the changes or discard them?"),
47 i18n("Apply Settings"),
48 KStandardGuiItem::apply(),
49 KStandardGuiItem::discard(),
50 KStandardGuiItem::cancel());
51
52 switch (queryUser) {
53 case KMessageBox::PrimaryAction:
54 return moduleSave(module);
55
56 case KMessageBox::SecondaryAction:
57 module->load();
58 return true;
59
60 case KMessageBox::Cancel:
61 return false;
62
63 default:
64 Q_ASSERT(false);
65 return false;
66 }
67}
68
69void KCMultiDialogPrivate::slotCurrentPageChanged(KPageWidgetItem *current, KPageWidgetItem *previous)
70{
71 KCModule *previousModule = nullptr;
72 for (int i = 0; i < modules.count(); ++i) {
73 if (modules[i].item == previous) {
74 previousModule = modules[i].kcm;
75 }
76 }
77
78 // Delete global margins and spacing, since we want the contents to
79 // be able to touch the edges of the window
80 q->layout()->setContentsMargins(0, 0, 0, 0);
81
82 const KPageWidget *pageWidget = q->pageWidget();
83 pageWidget->layout()->setSpacing(0);
84
85 // Then, we set the margins for the title header and the buttonBox footer
86 const QStyle *style = q->style();
87 const QMargins layoutMargins = QMargins(style->pixelMetric(QStyle::PM_LayoutLeftMargin),
88 style->pixelMetric(QStyle::PM_LayoutTopMargin),
89 style->pixelMetric(QStyle::PM_LayoutRightMargin),
90 style->pixelMetric(QStyle::PM_LayoutBottomMargin));
91
92 if (pageWidget->pageHeader()) {
93 pageWidget->pageHeader()->setContentsMargins(layoutMargins);
94 }
95
96 // Do not set buttonBox's top margin as that space will be covered by the content's bottom margin
97 q->buttonBox()->setContentsMargins(layoutMargins.left(), 0, layoutMargins.right(), layoutMargins.bottom());
98
99 q->blockSignals(true);
100 q->setCurrentPage(previous);
101
102 if (resolveChanges(module: previousModule)) {
103 q->setCurrentPage(current);
104 }
105 q->blockSignals(false);
106
107 // We need to get the state of the now active module
108 clientChanged();
109}
110
111void KCMultiDialogPrivate::clientChanged()
112{
113 // Get the current module
114 KCModule *activeModule = nullptr;
115 bool scheduleFirstShow = false;
116 for (int i = 0; i < modules.count(); ++i) {
117 if (modules[i].item == q->currentPage()) {
118 activeModule = modules[i].kcm;
119 scheduleFirstShow = activeModule && modules[i].firstShow;
120 break;
121 }
122 }
123
124 // When we first show a module, we call the load method
125 // Just in case we have multiple loadModule calls in a row, the current module could change
126 // Meaning we wait for the next tick, check the active module and call load if needed
127 if (scheduleFirstShow) {
128 QTimer::singleShot(0, q, [this]() {
129 for (int i = 0; i < modules.count(); ++i) {
130 if (modules[i].firstShow && modules[i].kcm && modules[i].item == q->currentPage()) {
131 modules[i].kcm->load();
132 modules[i].firstShow = false;
133 }
134 }
135 });
136 }
137
138 const bool change = activeModule && activeModule->needsSave();
139 const bool defaulted = activeModule && activeModule->representsDefaults();
140 const auto buttons = activeModule ? activeModule->buttons() : KCModule::NoAdditionalButton;
141
142 QPushButton *resetButton = q->buttonBox()->button(QDialogButtonBox::Reset);
143 if (resetButton) {
144 resetButton->setVisible(buttons & KCModule::Apply);
145 resetButton->setEnabled(change);
146 }
147
148 QPushButton *applyButton = q->buttonBox()->button(QDialogButtonBox::Apply);
149 if (applyButton) {
150 applyButton->setVisible(buttons & KCModule::Apply);
151 applyButton->setEnabled(change);
152 }
153
154 QPushButton *cancelButton = q->buttonBox()->button(QDialogButtonBox::Cancel);
155 if (cancelButton) {
156 cancelButton->setVisible(buttons & KCModule::Apply);
157 }
158
159 QPushButton *okButton = q->buttonBox()->button(QDialogButtonBox::Ok);
160 if (okButton) {
161 okButton->setVisible(buttons & KCModule::Apply);
162 }
163
164 QPushButton *closeButton = q->buttonBox()->button(QDialogButtonBox::Close);
165 if (closeButton) {
166 closeButton->setHidden(buttons & KCModule::Apply);
167 }
168
169 QPushButton *helpButton = q->buttonBox()->button(QDialogButtonBox::Help);
170 if (helpButton) {
171 helpButton->setVisible(buttons & KCModule::Help);
172 }
173
174 QPushButton *defaultButton = q->buttonBox()->button(QDialogButtonBox::RestoreDefaults);
175 if (defaultButton) {
176 defaultButton->setVisible(buttons & KCModule::Default);
177 defaultButton->setEnabled(!defaulted);
178 }
179}
180
181void KCMultiDialogPrivate::updateHeader(bool use, const QString &message)
182{
183 KPageWidgetItem *item = q->currentPage();
184 const auto findIt = std::find_if(modules.cbegin(), modules.cend(), [item](const CreatedModule &module) {
185 return module.item == item;
186 });
187 Q_ASSERT(findIt != modules.cend());
188
189 KCModule *kcm = findIt->kcm;
190 const QString moduleName = kcm->metaData().name();
191 const QString icon = kcm->metaData().iconName();
192
193 if (use) {
194 item->setHeader(QStringLiteral("<b>") + moduleName + QStringLiteral("</b><br><i>") + message + QStringLiteral("</i>"));
195 item->setIcon(KIconUtils::addOverlay(QIcon::fromTheme(icon), QIcon::fromTheme(QStringLiteral("dialog-warning")), Qt::BottomRightCorner));
196 } else {
197 item->setHeader(moduleName);
198 item->setIcon(QIcon::fromTheme(icon));
199 }
200}
201
202void KCMultiDialogPrivate::init()
203{
204 q->setFaceType(KPageDialog::Auto);
205 q->setWindowTitle(i18n("Configure"));
206 q->setModal(false);
207
208 QDialogButtonBox *buttonBox = new QDialogButtonBox(q);
209 buttonBox->setStandardButtons(QDialogButtonBox::Help | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Cancel | QDialogButtonBox::Apply
210 | QDialogButtonBox::Close | QDialogButtonBox::Ok | QDialogButtonBox::Reset);
211 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::ok());
212 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel());
213 KGuiItem::assign(buttonBox->button(QDialogButtonBox::RestoreDefaults), KStandardGuiItem::defaults());
214 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Apply), KStandardGuiItem::apply());
215 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Close), KStandardGuiItem::close());
216 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Reset), KStandardGuiItem::reset());
217 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Help), KStandardGuiItem::help());
218 buttonBox->button(QDialogButtonBox::Close)->setVisible(false);
219 buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false);
220 buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
221
222 q->connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, q, &KCMultiDialog::slotApplyClicked);
223 q->connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, q, &KCMultiDialog::slotOkClicked);
224 q->connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, q, &KCMultiDialog::slotDefaultClicked);
225 q->connect(buttonBox->button(QDialogButtonBox::Help), &QAbstractButton::clicked, q, &KCMultiDialog::slotHelpClicked);
226 q->connect(buttonBox->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, q, &KCMultiDialog::slotUser1Clicked);
227
228 q->setButtonBox(buttonBox);
229 q->connect(q, &KPageDialog::currentPageChanged, q, [this](KPageWidgetItem *current, KPageWidgetItem *before) {
230 slotCurrentPageChanged(current, before);
231 });
232}
233
234KCMultiDialog::KCMultiDialog(QWidget *parent)
235 : KPageDialog(parent)
236 , d(new KCMultiDialogPrivate(this))
237{
238 d->init();
239}
240
241KCMultiDialog::~KCMultiDialog() = default;
242
243void KCMultiDialog::showEvent(QShowEvent *ev)
244{
245 KPageDialog::showEvent(ev);
246 adjustSize();
247 /**
248 * adjustSize() relies on sizeHint but is limited to 2/3 of the desktop size
249 * Workaround for https://bugreports.qt.io/browse/QTBUG-3459
250 *
251 * We adjust the size after passing the show event
252 * because otherwise window pos is set to (0,0)
253 */
254
255 const QSize maxSize = screen()->availableGeometry().size();
256 resize(qMin(sizeHint().width(), maxSize.width()), qMin(sizeHint().height(), maxSize.height()));
257}
258
259void KCMultiDialog::slotDefaultClicked()
260{
261 const KPageWidgetItem *item = currentPage();
262 if (!item) {
263 return;
264 }
265
266 for (int i = 0; i < d->modules.count(); ++i) {
267 if (d->modules[i].item == item) {
268 d->modules[i].kcm->defaults();
269 d->clientChanged();
270 return;
271 }
272 }
273}
274
275void KCMultiDialog::slotUser1Clicked()
276{
277 const KPageWidgetItem *item = currentPage();
278 if (!item) {
279 return;
280 }
281
282 for (int i = 0; i < d->modules.count(); ++i) {
283 if (d->modules[i].item == item) {
284 d->modules[i].kcm->load();
285 d->clientChanged();
286 return;
287 }
288 }
289}
290
291bool KCMultiDialogPrivate::moduleSave(KCModule *module)
292{
293 if (!module) {
294 return false;
295 }
296
297 module->save();
298 return true;
299}
300
301void KCMultiDialogPrivate::apply()
302{
303 for (const CreatedModule &module : std::as_const(modules)) {
304 KCModule *kcm = module.kcm;
305
306 if (kcm->needsSave()) {
307 kcm->save();
308 }
309 }
310
311 Q_EMIT q->configCommitted();
312}
313
314void KCMultiDialog::slotApplyClicked()
315{
316 QPushButton *applyButton = buttonBox()->button(QDialogButtonBox::Apply);
317 applyButton->setFocus();
318
319 d->apply();
320}
321
322void KCMultiDialog::slotOkClicked()
323{
324 QPushButton *okButton = buttonBox()->button(QDialogButtonBox::Ok);
325 okButton->setFocus();
326
327 d->apply();
328 accept();
329}
330
331void KCMultiDialog::slotHelpClicked()
332{
333 const KPageWidgetItem *item = currentPage();
334 if (!item) {
335 return;
336 }
337
338 QString docPath;
339 for (int i = 0; i < d->modules.count(); ++i) {
340 if (d->modules[i].item == item) {
341 if (docPath.isEmpty()) {
342 docPath = d->modules[i].kcm->metaData().value(QStringLiteral("X-DocPath"));
343 }
344 break;
345 }
346 }
347
348 const QUrl docUrl = QUrl(QStringLiteral("help:/")).resolved(QUrl(docPath)); // same code as in KHelpClient::invokeHelp
349 const QString docUrlScheme = docUrl.scheme();
350 const QString helpExec = QStandardPaths::findExecutable(QStringLiteral("khelpcenter"));
351 const bool foundExec = !helpExec.isEmpty();
352 if (!foundExec) {
353 qCDebug(KCMUTILS_LOG) << "Couldn't find khelpcenter executable in PATH.";
354 }
355 if (foundExec && (docUrlScheme == QLatin1String("man") || docUrlScheme == QLatin1String("info"))) {
356 QProcess::startDetached(helpExec, QStringList() << docUrl.toString());
357 } else {
358 QDesktopServices::openUrl(docUrl);
359 }
360}
361
362void KCMultiDialog::closeEvent(QCloseEvent *event)
363{
364 KPageDialog::closeEvent(event);
365
366 for (auto &module : d->modules) {
367 delete module.kcm;
368 module.kcm = nullptr;
369 }
370}
371
372KPageWidgetItem *KCMultiDialog::addModule(const KPluginMetaData &metaData, const QVariantList &args)
373{
374 // Create the scroller
375 auto *moduleScroll = new UnboundScrollArea(this);
376 // Prepare the scroll area
377 moduleScroll->setWidgetResizable(true);
378 moduleScroll->setFrameStyle(QFrame::NoFrame);
379 moduleScroll->viewport()->setAutoFillBackground(false);
380
381 KCModule *kcm = KCModuleLoader::loadModule(metaData, moduleScroll, args);
382 moduleScroll->setWidget(kcm->widget());
383
384 KPageWidgetItem *item = new KPageWidgetItem(moduleScroll, metaData.name());
385
386 KCMultiDialogPrivate::CreatedModule createdModule;
387 createdModule.kcm = kcm;
388 createdModule.item = item;
389 d->modules.append(createdModule);
390
391 if (qobject_cast<KCModuleQml *>(kcm)) {
392 item->setHeaderVisible(false);
393 }
394
395 item->setHeader(metaData.name());
396 item->setIcon(QIcon::fromTheme(metaData.iconName()));
397 const int weight = metaData.rawData().value(QStringLiteral("X-KDE-Weight")).toInt();
398 item->setProperty("_k_weight", weight);
399
400 bool updateCurrentPage = false;
401 const KPageWidgetModel *model = qobject_cast<const KPageWidgetModel *>(pageWidget()->model());
402 Q_ASSERT(model);
403 const int siblingCount = model->rowCount();
404 int row = 0;
405 for (; row < siblingCount; ++row) {
406 KPageWidgetItem *siblingItem = model->item(model->index(row, 0));
407 if (siblingItem->property("_k_weight").toInt() > weight) {
408 // the item we found is heavier than the new module
409 // qDebug() << "adding KCM " << item->name() << " before " << siblingItem->name();
410 insertPage(siblingItem, item);
411 if (siblingItem == currentPage()) {
412 updateCurrentPage = true;
413 }
414
415 break;
416 }
417 }
418 if (row == siblingCount) {
419 // the new module is either the first or the heaviest item
420 // qDebug() << "adding KCM " << item->name() << " at the top level";
421 addPage(item);
422 }
423
424 connect(kcm, &KCModule::needsSaveChanged, this, [this]() {
425 d->clientChanged();
426 });
427
428 if (d->modules.count() == 1 || updateCurrentPage) {
429 setCurrentPage(item);
430 d->clientChanged();
431 }
432 return item;
433}
434
435void KCMultiDialog::clear()
436{
437 for (int i = 0; i < d->modules.count(); ++i) {
438 removePage(d->modules[i].item);
439 }
440
441 d->modules.clear();
442
443 d->clientChanged();
444}
445
446void KCMultiDialog::setDefaultsIndicatorsVisible(bool show)
447{
448 for (const auto &module : std::as_const(d->modules)) {
449 module.kcm->setDefaultsIndicatorsVisible(show);
450 }
451}
452
453#include "moc_kcmultidialog.cpp"
454

source code of kcmutils/src/kcmultidialog.cpp