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 <QScrollBar>
26#include <QStandardPaths>
27#include <QStringList>
28#include <QStyle>
29#include <QTimer>
30#include <QUrl>
31
32#include <KGuiItem>
33#include <KIconUtils>
34#include <KLocalizedString>
35#include <KMessageBox>
36#include <KPageWidgetModel>
37
38bool KCMultiDialogPrivate::resolveChanges(KCModule *module)
39{
40 if (!module || !module->needsSave()) {
41 return true;
42 }
43
44 // Let the user decide
45 const int queryUser = KMessageBox::warningTwoActionsCancel(parent: q,
46 i18n("The settings of the current module have changed.\n"
47 "Do you want to apply the changes or discard them?"),
48 i18n("Apply Settings"),
49 primaryAction: KStandardGuiItem::apply(),
50 secondaryAction: KStandardGuiItem::discard(),
51 cancelAction: KStandardGuiItem::cancel());
52
53 switch (queryUser) {
54 case KMessageBox::PrimaryAction:
55 return moduleSave(module);
56
57 case KMessageBox::SecondaryAction:
58 module->load();
59 return true;
60
61 case KMessageBox::Cancel:
62 return false;
63
64 default:
65 Q_ASSERT(false);
66 return false;
67 }
68}
69
70void KCMultiDialogPrivate::slotCurrentPageChanged(KPageWidgetItem *current, KPageWidgetItem *previous)
71{
72 KCModule *previousModule = nullptr;
73 for (int i = 0; i < modules.count(); ++i) {
74 if (modules[i].item == previous) {
75 previousModule = modules[i].kcm;
76 }
77 }
78
79 // Delete global margins and spacing, since we want the contents to
80 // be able to touch the edges of the window
81 q->layout()->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
82
83 const KPageWidget *pageWidget = q->pageWidget();
84 pageWidget->layout()->setSpacing(0);
85
86 // Then, we set the margins for the title header and the buttonBox footer
87 const QStyle *style = q->style();
88 const QMargins layoutMargins = QMargins(style->pixelMetric(metric: QStyle::PM_LayoutLeftMargin),
89 style->pixelMetric(metric: QStyle::PM_LayoutTopMargin),
90 style->pixelMetric(metric: QStyle::PM_LayoutRightMargin),
91 style->pixelMetric(metric: QStyle::PM_LayoutBottomMargin));
92
93 if (pageWidget->pageHeader()) {
94 pageWidget->pageHeader()->setContentsMargins(layoutMargins);
95 }
96
97 q->buttonBox()->setContentsMargins(left: layoutMargins.left(), top: layoutMargins.top(), right: layoutMargins.right(), bottom: layoutMargins.bottom());
98
99 q->blockSignals(b: true);
100 q->setCurrentPage(previous);
101
102 if (resolveChanges(module: previousModule)) {
103 q->setCurrentPage(current);
104 }
105 q->blockSignals(b: 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(interval: 0, receiver: q, slot: [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].firstShow = false;
132 modules[i].kcm->load();
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(which: QDialogButtonBox::Reset);
143 if (resetButton) {
144 resetButton->setVisible(buttons & KCModule::Apply);
145 resetButton->setEnabled(change);
146 }
147
148 QPushButton *applyButton = q->buttonBox()->button(which: QDialogButtonBox::Apply);
149 if (applyButton) {
150 applyButton->setVisible(buttons & KCModule::Apply);
151 applyButton->setEnabled(change);
152 }
153
154 QPushButton *cancelButton = q->buttonBox()->button(which: QDialogButtonBox::Cancel);
155 if (cancelButton) {
156 cancelButton->setVisible(buttons & KCModule::Apply);
157 }
158
159 QPushButton *okButton = q->buttonBox()->button(which: QDialogButtonBox::Ok);
160 if (okButton) {
161 okButton->setVisible(buttons & KCModule::Apply);
162 }
163
164 QPushButton *closeButton = q->buttonBox()->button(which: QDialogButtonBox::Close);
165 if (closeButton) {
166 closeButton->setHidden(buttons & KCModule::Apply);
167 }
168
169 QPushButton *helpButton = q->buttonBox()->button(which: QDialogButtonBox::Help);
170 if (helpButton) {
171 helpButton->setVisible(buttons & KCModule::Help);
172 }
173
174 QPushButton *defaultButton = q->buttonBox()->button(which: 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(first: modules.cbegin(), last: modules.cend(), pred: [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(icon: QIcon::fromTheme(name: icon), overlay: QIcon::fromTheme(QStringLiteral("dialog-warning")), position: Qt::BottomRightCorner));
196 } else {
197 item->setHeader(moduleName);
198 item->setIcon(QIcon::fromTheme(name: icon));
199 }
200}
201
202void KCMultiDialogPrivate::updateScrollAreaFocusPolicy()
203{
204 KPageWidgetItem *item = q->currentPage();
205 if (!item) {
206 return;
207 }
208 UnboundScrollArea *moduleScroll = qobject_cast<UnboundScrollArea *>(object: item->widget());
209 if (moduleScroll) {
210 bool scrollbarVisible = moduleScroll->horizontalScrollBar()->isVisible() || moduleScroll->verticalScrollBar()->isVisible();
211 moduleScroll->setFocusPolicy(scrollbarVisible ? Qt::FocusPolicy::StrongFocus : Qt::FocusPolicy::NoFocus);
212 }
213}
214
215void KCMultiDialogPrivate::init()
216{
217 q->setFaceType(KPageDialog::Auto);
218 q->setWindowTitle(i18n("Configure"));
219 q->setModal(false);
220
221 QDialogButtonBox *buttonBox = new QDialogButtonBox(q);
222 buttonBox->setStandardButtons(QDialogButtonBox::Help | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Cancel | QDialogButtonBox::Apply
223 | QDialogButtonBox::Close | QDialogButtonBox::Ok | QDialogButtonBox::Reset);
224 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Ok), item: KStandardGuiItem::ok());
225 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Cancel), item: KStandardGuiItem::cancel());
226 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::RestoreDefaults), item: KStandardGuiItem::defaults());
227 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Apply), item: KStandardGuiItem::apply());
228 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Close), item: KStandardGuiItem::close());
229 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Reset), item: KStandardGuiItem::reset());
230 KGuiItem::assign(button: buttonBox->button(which: QDialogButtonBox::Help), item: KStandardGuiItem::help());
231 buttonBox->button(which: QDialogButtonBox::Close)->setVisible(false);
232 buttonBox->button(which: QDialogButtonBox::Reset)->setEnabled(false);
233 buttonBox->button(which: QDialogButtonBox::Apply)->setEnabled(false);
234
235 q->connect(sender: buttonBox->button(which: QDialogButtonBox::Apply), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotApplyClicked);
236 q->connect(sender: buttonBox->button(which: QDialogButtonBox::Ok), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotOkClicked);
237 q->connect(sender: buttonBox->button(which: QDialogButtonBox::RestoreDefaults), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotDefaultClicked);
238 q->connect(sender: buttonBox->button(which: QDialogButtonBox::Help), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotHelpClicked);
239 q->connect(sender: buttonBox->button(which: QDialogButtonBox::Reset), signal: &QAbstractButton::clicked, context: q, slot: &KCMultiDialog::slotUser1Clicked);
240
241 q->setButtonBox(buttonBox);
242 q->connect(sender: q, signal: &KPageDialog::currentPageChanged, context: q, slot: [this](KPageWidgetItem *current, KPageWidgetItem *before) {
243 slotCurrentPageChanged(current, previous: before);
244 });
245}
246
247KCMultiDialog::KCMultiDialog(QWidget *parent)
248 : KPageDialog(parent)
249 , d(new KCMultiDialogPrivate(this))
250{
251 d->init();
252}
253
254KCMultiDialog::~KCMultiDialog() = default;
255
256void KCMultiDialog::showEvent(QShowEvent *ev)
257{
258 KPageDialog::showEvent(ev);
259 adjustSize();
260 /*
261 * adjustSize() relies on sizeHint but is limited to 2/3 of the desktop size
262 * Workaround for https://bugreports.qt.io/browse/QTBUG-3459
263 *
264 * We adjust the size after passing the show event
265 * because otherwise window pos is set to (0,0)
266 */
267
268 const QSize maxSize = screen()->availableGeometry().size();
269 resize(w: qMin(a: sizeHint().width(), b: maxSize.width()), h: qMin(a: sizeHint().height(), b: maxSize.height()));
270}
271
272void KCMultiDialog::slotDefaultClicked()
273{
274 const KPageWidgetItem *item = currentPage();
275 if (!item) {
276 return;
277 }
278
279 for (int i = 0; i < d->modules.count(); ++i) {
280 if (d->modules[i].item == item) {
281 d->modules[i].kcm->defaults();
282 d->clientChanged();
283 return;
284 }
285 }
286}
287
288void KCMultiDialog::slotUser1Clicked()
289{
290 const KPageWidgetItem *item = currentPage();
291 if (!item) {
292 return;
293 }
294
295 for (int i = 0; i < d->modules.count(); ++i) {
296 if (d->modules[i].item == item) {
297 d->modules[i].kcm->load();
298 d->clientChanged();
299 return;
300 }
301 }
302}
303
304bool KCMultiDialogPrivate::moduleSave(KCModule *module)
305{
306 if (!module) {
307 return false;
308 }
309
310 module->save();
311 return true;
312}
313
314void KCMultiDialogPrivate::apply()
315{
316 for (const CreatedModule &module : std::as_const(t&: modules)) {
317 KCModule *kcm = module.kcm;
318
319 if (kcm && kcm->needsSave()) {
320 kcm->save();
321 }
322 }
323
324 Q_EMIT q->configCommitted();
325}
326
327void KCMultiDialog::slotApplyClicked()
328{
329 QPushButton *applyButton = buttonBox()->button(which: QDialogButtonBox::Apply);
330 applyButton->setFocus();
331
332 d->apply();
333}
334
335void KCMultiDialog::slotOkClicked()
336{
337 QPushButton *okButton = buttonBox()->button(which: QDialogButtonBox::Ok);
338 okButton->setFocus();
339
340 d->apply();
341 accept();
342}
343
344void KCMultiDialog::slotHelpClicked()
345{
346 const KPageWidgetItem *item = currentPage();
347 if (!item) {
348 return;
349 }
350
351 QString docPath;
352 for (int i = 0; i < d->modules.count(); ++i) {
353 if (d->modules[i].item == item) {
354 if (docPath.isEmpty()) {
355 docPath = d->modules[i].kcm->metaData().value(QStringLiteral("X-DocPath"));
356 }
357 break;
358 }
359 }
360
361 const QUrl docUrl = QUrl(QStringLiteral("help:/")).resolved(relative: QUrl(docPath)); // same code as in KHelpClient::invokeHelp
362 const QString docUrlScheme = docUrl.scheme();
363 const QString helpExec = QStandardPaths::findExecutable(QStringLiteral("khelpcenter"));
364 const bool foundExec = !helpExec.isEmpty();
365 if (!foundExec) {
366 qCDebug(KCMUTILS_LOG) << "Couldn't find khelpcenter executable in PATH.";
367 }
368 if (foundExec && (docUrlScheme == QLatin1String("man") || docUrlScheme == QLatin1String("info"))) {
369 QProcess::startDetached(program: helpExec, arguments: QStringList() << docUrl.toString());
370 } else {
371 QDesktopServices::openUrl(url: docUrl);
372 }
373}
374
375void KCMultiDialog::closeEvent(QCloseEvent *event)
376{
377 KPageDialog::closeEvent(event);
378
379 for (auto &module : d->modules) {
380 delete module.kcm;
381 module.kcm = nullptr;
382 }
383}
384
385KPageWidgetItem *KCMultiDialog::addModule(const KPluginMetaData &metaData, const QVariantList &args)
386{
387 // Create the scroller
388 auto *moduleScroll = new UnboundScrollArea(this);
389 // Prepare the scroll area
390 moduleScroll->setWidgetResizable(true);
391 moduleScroll->setFrameStyle(QFrame::NoFrame);
392 moduleScroll->viewport()->setAutoFillBackground(false);
393 moduleScroll->setAccessibleName(i18ndc(domain: "kcmutils", context: "@other accessible name for view that can be scrolled", text: "Scrollable area"));
394
395 KCModule *kcm = KCModuleLoader::loadModule(metaData, parent: moduleScroll, args);
396 moduleScroll->setWidget(kcm->widget());
397
398 KPageWidgetItem *item = new KPageWidgetItem(moduleScroll, metaData.name());
399
400 KCMultiDialogPrivate::CreatedModule createdModule;
401 createdModule.kcm = kcm;
402 createdModule.item = item;
403 d->modules.append(t: createdModule);
404
405 if (qobject_cast<KCModuleQml *>(object: kcm)) {
406 item->setHeaderVisible(false);
407 }
408
409 item->setHeader(metaData.name());
410 item->setIcon(QIcon::fromTheme(name: metaData.iconName()));
411 const int weight = metaData.rawData().value(QStringLiteral("X-KDE-Weight")).toInt();
412 item->setProperty(name: "_k_weight", value: weight);
413
414 bool updateCurrentPage = false;
415 const KPageWidgetModel *model = qobject_cast<const KPageWidgetModel *>(object: pageWidget()->model());
416 Q_ASSERT(model);
417 const int siblingCount = model->rowCount();
418 int row = 0;
419 for (; row < siblingCount; ++row) {
420 KPageWidgetItem *siblingItem = model->item(index: model->index(row, column: 0));
421 if (siblingItem->property(name: "_k_weight").toInt() > weight) {
422 // the item we found is heavier than the new module
423 // qDebug() << "adding KCM " << item->name() << " before " << siblingItem->name();
424 insertPage(before: siblingItem, item);
425 if (siblingItem == currentPage()) {
426 updateCurrentPage = true;
427 }
428
429 break;
430 }
431 }
432 if (row == siblingCount) {
433 // the new module is either the first or the heaviest item
434 // qDebug() << "adding KCM " << item->name() << " at the top level";
435 addPage(item);
436 }
437
438 connect(sender: kcm, signal: &KCModule::needsSaveChanged, context: this, slot: [this]() {
439 d->clientChanged();
440 });
441
442 if (d->modules.count() == 1 || updateCurrentPage) {
443 setCurrentPage(item);
444 d->clientChanged();
445 }
446 moduleScroll->horizontalScrollBar()->installEventFilter(filterObj: this);
447 moduleScroll->verticalScrollBar()->installEventFilter(filterObj: this);
448 d->updateScrollAreaFocusPolicy();
449 return item;
450}
451
452void KCMultiDialog::clear()
453{
454 for (int i = 0; i < d->modules.count(); ++i) {
455 removePage(item: d->modules[i].item);
456 }
457
458 d->modules.clear();
459
460 d->clientChanged();
461}
462
463void KCMultiDialog::setDefaultsIndicatorsVisible(bool show)
464{
465 for (const auto &module : std::as_const(t&: d->modules)) {
466 module.kcm->setDefaultsIndicatorsVisible(show);
467 }
468}
469
470bool KCMultiDialog::eventFilter(QObject *watched, QEvent *event)
471{
472 if ((event->type() == QEvent::Show || event->type() == QEvent::Hide) && currentPage()) {
473 UnboundScrollArea *moduleScroll = qobject_cast<UnboundScrollArea *>(object: currentPage()->widget());
474 if (moduleScroll && (watched == moduleScroll->horizontalScrollBar() || watched == moduleScroll->verticalScrollBar())) {
475 d->updateScrollAreaFocusPolicy();
476 }
477 }
478 return KPageDialog::eventFilter(watched, event);
479}
480
481#include "moc_kcmultidialog.cpp"
482

source code of kcmutils/src/kcmultidialog.cpp