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(parent: 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 primaryAction: KStandardGuiItem::apply(),
49 secondaryAction: KStandardGuiItem::discard(),
50 cancelAction: 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(left: 0, top: 0, right: 0, bottom: 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(metric: QStyle::PM_LayoutLeftMargin),
88 style->pixelMetric(metric: QStyle::PM_LayoutTopMargin),
89 style->pixelMetric(metric: QStyle::PM_LayoutRightMargin),
90 style->pixelMetric(metric: QStyle::PM_LayoutBottomMargin));
91
92 if (pageWidget->pageHeader()) {
93 pageWidget->pageHeader()->setContentsMargins(layoutMargins);
94 }
95
96 q->buttonBox()->setContentsMargins(left: layoutMargins.left(), top: layoutMargins.top(), right: layoutMargins.right(), bottom: layoutMargins.bottom());
97
98 q->blockSignals(b: true);
99 q->setCurrentPage(previous);
100
101 if (resolveChanges(module: previousModule)) {
102 q->setCurrentPage(current);
103 }
104 q->blockSignals(b: false);
105
106 // We need to get the state of the now active module
107 clientChanged();
108}
109
110void KCMultiDialogPrivate::clientChanged()
111{
112 // Get the current module
113 KCModule *activeModule = nullptr;
114 bool scheduleFirstShow = false;
115 for (int i = 0; i < modules.count(); ++i) {
116 if (modules[i].item == q->currentPage()) {
117 activeModule = modules[i].kcm;
118 scheduleFirstShow = activeModule && modules[i].firstShow;
119 break;
120 }
121 }
122
123 // When we first show a module, we call the load method
124 // Just in case we have multiple loadModule calls in a row, the current module could change
125 // Meaning we wait for the next tick, check the active module and call load if needed
126 if (scheduleFirstShow) {
127 QTimer::singleShot(interval: 0, receiver: q, slot: [this]() {
128 for (int i = 0; i < modules.count(); ++i) {
129 if (modules[i].firstShow && modules[i].kcm && modules[i].item == q->currentPage()) {
130 modules[i].kcm->load();
131 modules[i].firstShow = false;
132 }
133 }
134 });
135 }
136
137 const bool change = activeModule && activeModule->needsSave();
138 const bool defaulted = activeModule && activeModule->representsDefaults();
139 const auto buttons = activeModule ? activeModule->buttons() : KCModule::NoAdditionalButton;
140
141 QPushButton *resetButton = q->buttonBox()->button(which: QDialogButtonBox::Reset);
142 if (resetButton) {
143 resetButton->setVisible(buttons & KCModule::Apply);
144 resetButton->setEnabled(change);
145 }
146
147 QPushButton *applyButton = q->buttonBox()->button(which: QDialogButtonBox::Apply);
148 if (applyButton) {
149 applyButton->setVisible(buttons & KCModule::Apply);
150 applyButton->setEnabled(change);
151 }
152
153 QPushButton *cancelButton = q->buttonBox()->button(which: QDialogButtonBox::Cancel);
154 if (cancelButton) {
155 cancelButton->setVisible(buttons & KCModule::Apply);
156 }
157
158 QPushButton *okButton = q->buttonBox()->button(which: QDialogButtonBox::Ok);
159 if (okButton) {
160 okButton->setVisible(buttons & KCModule::Apply);
161 }
162
163 QPushButton *closeButton = q->buttonBox()->button(which: QDialogButtonBox::Close);
164 if (closeButton) {
165 closeButton->setHidden(buttons & KCModule::Apply);
166 }
167
168 QPushButton *helpButton = q->buttonBox()->button(which: QDialogButtonBox::Help);
169 if (helpButton) {
170 helpButton->setVisible(buttons & KCModule::Help);
171 }
172
173 QPushButton *defaultButton = q->buttonBox()->button(which: QDialogButtonBox::RestoreDefaults);
174 if (defaultButton) {
175 defaultButton->setVisible(buttons & KCModule::Default);
176 defaultButton->setEnabled(!defaulted);
177 }
178}
179
180void KCMultiDialogPrivate::updateHeader(bool use, const QString &message)
181{
182 KPageWidgetItem *item = q->currentPage();
183 const auto findIt = std::find_if(first: modules.cbegin(), last: modules.cend(), pred: [item](const CreatedModule &module) {
184 return module.item == item;
185 });
186 Q_ASSERT(findIt != modules.cend());
187
188 KCModule *kcm = findIt->kcm;
189 const QString moduleName = kcm->metaData().name();
190 const QString icon = kcm->metaData().iconName();
191
192 if (use) {
193 item->setHeader(QStringLiteral("<b>") + moduleName + QStringLiteral("</b><br><i>") + message + QStringLiteral("</i>"));
194 item->setIcon(KIconUtils::addOverlay(icon: QIcon::fromTheme(name: icon), overlay: QIcon::fromTheme(QStringLiteral("dialog-warning")), position: Qt::BottomRightCorner));
195 } else {
196 item->setHeader(moduleName);
197 item->setIcon(QIcon::fromTheme(name: icon));
198 }
199}
200
201void KCMultiDialogPrivate::init()
202{
203 q->setFaceType(KPageDialog::Auto);
204 q->setWindowTitle(i18n("Configure"));
205 q->setModal(false);
206
207 QDialogButtonBox *buttonBox = new QDialogButtonBox(q);
208 buttonBox->setStandardButtons(QDialogButtonBox::Help | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Cancel | QDialogButtonBox::Apply
209 | QDialogButtonBox::Close | QDialogButtonBox::Ok | QDialogButtonBox::Reset);
210 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Ok), item: KStandardGuiItem::ok());
211 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Cancel), item: KStandardGuiItem::cancel());
212 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::RestoreDefaults), item: KStandardGuiItem::defaults());
213 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Apply), item: KStandardGuiItem::apply());
214 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Close), item: KStandardGuiItem::close());
215 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Reset), item: KStandardGuiItem::reset());
216 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Help), item: KStandardGuiItem::help());
217 buttonBox->button(which: QDialogButtonBox::Close)->setVisible(false);
218 buttonBox->button(which: QDialogButtonBox::Reset)->setEnabled(false);
219 buttonBox->button(which: QDialogButtonBox::Apply)->setEnabled(false);
220
221 q->connect(sender: buttonBox->button(which: QDialogButtonBox::Apply), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotApplyClicked);
222 q->connect(sender: buttonBox->button(which: QDialogButtonBox::Ok), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotOkClicked);
223 q->connect(sender: buttonBox->button(which: QDialogButtonBox::RestoreDefaults), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotDefaultClicked);
224 q->connect(sender: buttonBox->button(which: QDialogButtonBox::Help), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotHelpClicked);
225 q->connect(sender: buttonBox->button(which: QDialogButtonBox::Reset), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotUser1Clicked);
226
227 q->setButtonBox(buttonBox);
228 q->connect(sender: q, signal: &KPageDialog::currentPageChanged, context: q, slot: [this](KPageWidgetItem *current, KPageWidgetItem *before) {
229 slotCurrentPageChanged(current, previous: before);
230 });
231}
232
233KCMultiDialog::KCMultiDialog(QWidget *parent)
234 : KPageDialog(parent)
235 , d(new KCMultiDialogPrivate(this))
236{
237 d->init();
238}
239
240KCMultiDialog::~KCMultiDialog() = default;
241
242void KCMultiDialog::showEvent(QShowEvent *ev)
243{
244 KPageDialog::showEvent(ev);
245 adjustSize();
246 /**
247 * adjustSize() relies on sizeHint but is limited to 2/3 of the desktop size
248 * Workaround for https://bugreports.qt.io/browse/QTBUG-3459
249 *
250 * We adjust the size after passing the show event
251 * because otherwise window pos is set to (0,0)
252 */
253
254 const QSize maxSize = screen()->availableGeometry().size();
255 resize(w: qMin(a: sizeHint().width(), b: maxSize.width()), h: qMin(a: sizeHint().height(), b: maxSize.height()));
256}
257
258void KCMultiDialog::slotDefaultClicked()
259{
260 const KPageWidgetItem *item = currentPage();
261 if (!item) {
262 return;
263 }
264
265 for (int i = 0; i < d->modules.count(); ++i) {
266 if (d->modules[i].item == item) {
267 d->modules[i].kcm->defaults();
268 d->clientChanged();
269 return;
270 }
271 }
272}
273
274void KCMultiDialog::slotUser1Clicked()
275{
276 const KPageWidgetItem *item = currentPage();
277 if (!item) {
278 return;
279 }
280
281 for (int i = 0; i < d->modules.count(); ++i) {
282 if (d->modules[i].item == item) {
283 d->modules[i].kcm->load();
284 d->clientChanged();
285 return;
286 }
287 }
288}
289
290bool KCMultiDialogPrivate::moduleSave(KCModule *module)
291{
292 if (!module) {
293 return false;
294 }
295
296 module->save();
297 return true;
298}
299
300void KCMultiDialogPrivate::apply()
301{
302 for (const CreatedModule &module : std::as_const(t&: modules)) {
303 KCModule *kcm = module.kcm;
304
305 if (kcm->needsSave()) {
306 kcm->save();
307 }
308 }
309
310 Q_EMIT q->configCommitted();
311}
312
313void KCMultiDialog::slotApplyClicked()
314{
315 QPushButton *applyButton = buttonBox()->button(which: QDialogButtonBox::Apply);
316 applyButton->setFocus();
317
318 d->apply();
319}
320
321void KCMultiDialog::slotOkClicked()
322{
323 QPushButton *okButton = buttonBox()->button(which: QDialogButtonBox::Ok);
324 okButton->setFocus();
325
326 d->apply();
327 accept();
328}
329
330void KCMultiDialog::slotHelpClicked()
331{
332 const KPageWidgetItem *item = currentPage();
333 if (!item) {
334 return;
335 }
336
337 QString docPath;
338 for (int i = 0; i < d->modules.count(); ++i) {
339 if (d->modules[i].item == item) {
340 if (docPath.isEmpty()) {
341 docPath = d->modules[i].kcm->metaData().value(QStringLiteral("X-DocPath"));
342 }
343 break;
344 }
345 }
346
347 const QUrl docUrl = QUrl(QStringLiteral("help:/")).resolved(relative: QUrl(docPath)); // same code as in KHelpClient::invokeHelp
348 const QString docUrlScheme = docUrl.scheme();
349 const QString helpExec = QStandardPaths::findExecutable(QStringLiteral("khelpcenter"));
350 const bool foundExec = !helpExec.isEmpty();
351 if (!foundExec) {
352 qCDebug(KCMUTILS_LOG) << "Couldn't find khelpcenter executable in PATH.";
353 }
354 if (foundExec && (docUrlScheme == QLatin1String("man") || docUrlScheme == QLatin1String("info"))) {
355 QProcess::startDetached(program: helpExec, arguments: QStringList() << docUrl.toString());
356 } else {
357 QDesktopServices::openUrl(url: docUrl);
358 }
359}
360
361void KCMultiDialog::closeEvent(QCloseEvent *event)
362{
363 KPageDialog::closeEvent(event);
364
365 for (auto &module : d->modules) {
366 delete module.kcm;
367 module.kcm = nullptr;
368 }
369}
370
371KPageWidgetItem *KCMultiDialog::addModule(const KPluginMetaData &metaData, const QVariantList &args)
372{
373 // Create the scroller
374 auto *moduleScroll = new UnboundScrollArea(this);
375 // Prepare the scroll area
376 moduleScroll->setWidgetResizable(true);
377 moduleScroll->setFrameStyle(QFrame::NoFrame);
378 moduleScroll->viewport()->setAutoFillBackground(false);
379
380 KCModule *kcm = KCModuleLoader::loadModule(metaData, parent: moduleScroll, args);
381 moduleScroll->setWidget(kcm->widget());
382
383 KPageWidgetItem *item = new KPageWidgetItem(moduleScroll, metaData.name());
384
385 KCMultiDialogPrivate::CreatedModule createdModule;
386 createdModule.kcm = kcm;
387 createdModule.item = item;
388 d->modules.append(t: createdModule);
389
390 if (qobject_cast<KCModuleQml *>(object: kcm)) {
391 item->setHeaderVisible(false);
392 }
393
394 item->setHeader(metaData.name());
395 item->setIcon(QIcon::fromTheme(name: metaData.iconName()));
396 const int weight = metaData.rawData().value(QStringLiteral("X-KDE-Weight")).toInt();
397 item->setProperty(name: "_k_weight", value: weight);
398
399 bool updateCurrentPage = false;
400 const KPageWidgetModel *model = qobject_cast<const KPageWidgetModel *>(object: pageWidget()->model());
401 Q_ASSERT(model);
402 const int siblingCount = model->rowCount();
403 int row = 0;
404 for (; row < siblingCount; ++row) {
405 KPageWidgetItem *siblingItem = model->item(index: model->index(row, column: 0));
406 if (siblingItem->property(name: "_k_weight").toInt() > weight) {
407 // the item we found is heavier than the new module
408 // qDebug() << "adding KCM " << item->name() << " before " << siblingItem->name();
409 insertPage(before: siblingItem, item);
410 if (siblingItem == currentPage()) {
411 updateCurrentPage = true;
412 }
413
414 break;
415 }
416 }
417 if (row == siblingCount) {
418 // the new module is either the first or the heaviest item
419 // qDebug() << "adding KCM " << item->name() << " at the top level";
420 addPage(item);
421 }
422
423 connect(sender: kcm, signal: &KCModule::needsSaveChanged, context: this, slot: [this]() {
424 d->clientChanged();
425 });
426
427 if (d->modules.count() == 1 || updateCurrentPage) {
428 setCurrentPage(item);
429 d->clientChanged();
430 }
431 return item;
432}
433
434void KCMultiDialog::clear()
435{
436 for (int i = 0; i < d->modules.count(); ++i) {
437 removePage(item: d->modules[i].item);
438 }
439
440 d->modules.clear();
441
442 d->clientChanged();
443}
444
445void KCMultiDialog::setDefaultsIndicatorsVisible(bool show)
446{
447 for (const auto &module : std::as_const(t&: d->modules)) {
448 module.kcm->setDefaultsIndicatorsVisible(show);
449 }
450}
451
452#include "moc_kcmultidialog.cpp"
453

source code of kcmutils/src/kcmultidialog.cpp