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 | |
37 | bool 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 | |
69 | void 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 | |
110 | void 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 | |
180 | void KCMultiDialogPrivate::(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 | |
201 | void 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 | |
233 | KCMultiDialog::KCMultiDialog(QWidget *parent) |
234 | : KPageDialog(parent) |
235 | , d(new KCMultiDialogPrivate(this)) |
236 | { |
237 | d->init(); |
238 | } |
239 | |
240 | KCMultiDialog::~KCMultiDialog() = default; |
241 | |
242 | void 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 | |
258 | void 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 | |
274 | void 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 | |
290 | bool KCMultiDialogPrivate::moduleSave(KCModule *module) |
291 | { |
292 | if (!module) { |
293 | return false; |
294 | } |
295 | |
296 | module->save(); |
297 | return true; |
298 | } |
299 | |
300 | void 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 | |
313 | void KCMultiDialog::slotApplyClicked() |
314 | { |
315 | QPushButton *applyButton = buttonBox()->button(which: QDialogButtonBox::Apply); |
316 | applyButton->setFocus(); |
317 | |
318 | d->apply(); |
319 | } |
320 | |
321 | void KCMultiDialog::slotOkClicked() |
322 | { |
323 | QPushButton *okButton = buttonBox()->button(which: QDialogButtonBox::Ok); |
324 | okButton->setFocus(); |
325 | |
326 | d->apply(); |
327 | accept(); |
328 | } |
329 | |
330 | void 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 | |
361 | void 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 | |
371 | KPageWidgetItem *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 | |
434 | void 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 | |
445 | void 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 | |