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(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 | |
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(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 | |
111 | void 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 | |
181 | void KCMultiDialogPrivate::(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 | |
202 | void 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 | |
234 | KCMultiDialog::KCMultiDialog(QWidget *parent) |
235 | : KPageDialog(parent) |
236 | , d(new KCMultiDialogPrivate(this)) |
237 | { |
238 | d->init(); |
239 | } |
240 | |
241 | KCMultiDialog::~KCMultiDialog() = default; |
242 | |
243 | void 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 | |
259 | void 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 | |
275 | void 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 | |
291 | bool KCMultiDialogPrivate::moduleSave(KCModule *module) |
292 | { |
293 | if (!module) { |
294 | return false; |
295 | } |
296 | |
297 | module->save(); |
298 | return true; |
299 | } |
300 | |
301 | void 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 | |
314 | void KCMultiDialog::slotApplyClicked() |
315 | { |
316 | QPushButton *applyButton = buttonBox()->button(QDialogButtonBox::Apply); |
317 | applyButton->setFocus(); |
318 | |
319 | d->apply(); |
320 | } |
321 | |
322 | void KCMultiDialog::slotOkClicked() |
323 | { |
324 | QPushButton *okButton = buttonBox()->button(QDialogButtonBox::Ok); |
325 | okButton->setFocus(); |
326 | |
327 | d->apply(); |
328 | accept(); |
329 | } |
330 | |
331 | void 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 | |
362 | void 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 | |
372 | KPageWidgetItem *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 | |
435 | void 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 | |
446 | void 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 | |