1 /* -*- mode: c++; c-basic-offset:4 -*-
2     crypto/gui/wizard.cpp
3 
4     This file is part of Kleopatra, the KDE keymanager
5     SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include <config-kleopatra.h>
11 
12 #include "wizard.h"
13 #include "wizardpage.h"
14 
15 #include <utils/kleo_assert.h>
16 
17 #include <Libkleo/Algorithm>
18 
19 #include <KGuiItem>
20 #include <KLocalizedString>
21 #include <QPushButton>
22 #include <KStandardGuiItem>
23 
24 #include <QDialogButtonBox>
25 #include <QFrame>
26 #include <QLabel>
27 #include <QStackedWidget>
28 #include <QTimer>
29 #include <QVBoxLayout>
30 #include "kleopatra_debug.h"
31 
32 #include <map>
33 #include <set>
34 
35 
36 using namespace Kleo::Crypto::Gui;
37 
38 class Wizard::Private
39 {
40     friend class ::Wizard;
41     Wizard *const q;
42 public:
43     explicit Private(Wizard *qq);
44     ~Private();
45 
46     void updateButtonStates();
47     bool isLastPage(int id) const;
48     int previousPage() const;
49     void updateHeader();
50 
51 private:
52     std::vector<int> pageOrder;
53     std::set<int> hiddenPages;
54     std::map<int, WizardPage *> idToPage;
55     int currentId = -1;
56     QStackedWidget *const stack;
57     QPushButton *nextButton = nullptr;
58     QPushButton *backButton = nullptr;
59     QPushButton *cancelButton = nullptr;
60     KGuiItem finishItem;
61     KGuiItem nextItem;
62     QFrame *titleFrame = nullptr;
63     QLabel *titleLabel = nullptr;
64     QLabel *subTitleLabel = nullptr;
65     QFrame *explanationFrame = nullptr;
66     QLabel *explanationLabel = nullptr;
67     QTimer *nextPageTimer = nullptr;
68 };
69 
Private(Wizard * qq)70 Wizard::Private::Private(Wizard *qq)
71     : q(qq), stack(new QStackedWidget)
72 {
73     nextPageTimer = new QTimer(q);
74     nextPageTimer->setInterval(0);
75     connect(nextPageTimer, &QTimer::timeout, q, &Wizard::next);
76     nextItem = KGuiItem(i18n("&Next"));
77     finishItem = KStandardGuiItem::ok();
78     auto const top = new QVBoxLayout(q);
79     top->setContentsMargins(0, 0, 0, 0);
80     titleFrame = new QFrame;
81     titleFrame->setFrameShape(QFrame::StyledPanel);
82     titleFrame->setAutoFillBackground(true);
83     titleFrame->setBackgroundRole(QPalette::Base);
84     auto const titleLayout = new QVBoxLayout(titleFrame);
85     titleLabel = new QLabel;
86     titleLayout->addWidget(titleLabel);
87     subTitleLabel = new QLabel;
88     subTitleLabel->setWordWrap(true);
89     titleLayout->addWidget(subTitleLabel);
90     top->addWidget(titleFrame);
91     titleFrame->setVisible(false);
92 
93     top->addWidget(stack);
94 
95     explanationFrame = new QFrame;
96     explanationFrame->setFrameShape(QFrame::StyledPanel);
97     explanationFrame->setAutoFillBackground(true);
98     explanationFrame->setBackgroundRole(QPalette::Base);
99     auto const explanationLayout = new QVBoxLayout(explanationFrame);
100     explanationLabel = new QLabel;
101     explanationLabel->setWordWrap(true);
102     explanationLayout->addWidget(explanationLabel);
103     top->addWidget(explanationFrame);
104     explanationFrame->setVisible(false);
105 
106     auto buttonWidget = new QWidget;
107     auto buttonLayout = new QHBoxLayout(buttonWidget);
108     auto const box = new QDialogButtonBox;
109 
110     cancelButton = box->addButton(QDialogButtonBox::Cancel);
111     q->connect(cancelButton, &QPushButton::clicked, q, &Wizard::reject);
112 
113     backButton = new QPushButton;
114     backButton->setText(i18n("Back"));
115     q->connect(backButton, &QPushButton::clicked, q, &Wizard::back);
116     box->addButton(backButton, QDialogButtonBox::ActionRole);
117 
118     nextButton = new QPushButton;
119     KGuiItem::assign(nextButton, nextItem);
120     q->connect(nextButton, &QPushButton::clicked, q, &Wizard::next);
121     box->addButton(nextButton, QDialogButtonBox::ActionRole);
122     buttonLayout->addWidget(box);
123 
124     top->addWidget(buttonWidget);
125 
126     q->connect(q, &Wizard::rejected, q, &Wizard::canceled);
127 }
128 
~Private()129 Wizard::Private::~Private()
130 {
131     qCDebug(KLEOPATRA_LOG);
132 }
133 
isLastPage(int id) const134 bool Wizard::Private::isLastPage(int id) const
135 {
136     return !pageOrder.empty() ? pageOrder.back() == id : false;
137 }
138 
updateButtonStates()139 void Wizard::Private::updateButtonStates()
140 {
141     const bool isLast = isLastPage(currentId);
142     const bool canGoToNext = q->canGoToNextPage();
143     WizardPage *const page = q->page(currentId);
144     const KGuiItem customNext = page ? page->customNextButton() : KGuiItem();
145     if (customNext.text().isEmpty() && customNext.icon().isNull()) {
146         KGuiItem::assign(nextButton, isLast ? finishItem : nextItem);
147     } else {
148         KGuiItem::assign(nextButton, customNext);
149     }
150     nextButton->setEnabled(canGoToNext);
151     cancelButton->setEnabled(!isLast || !canGoToNext);
152     backButton->setEnabled(q->canGoToPreviousPage());
153     if (page && page->autoAdvance() && page->isComplete()) {
154         nextPageTimer->start();
155     }
156 }
157 
updateHeader()158 void Wizard::Private::updateHeader()
159 {
160     WizardPage *const widget = q->page(currentId);
161     Q_ASSERT(!widget || stack->indexOf(widget) != -1);
162     if (widget) {
163         stack->setCurrentWidget(widget);
164     }
165     const QString title = widget ? widget->title() : QString();
166     const QString subTitle = widget ? widget->subTitle() : QString();
167     const QString explanation = widget ? widget->explanation() : QString();
168     titleFrame->setVisible(!title.isEmpty() || !subTitle.isEmpty() || !explanation.isEmpty());
169     titleLabel->setVisible(!title.isEmpty());
170     titleLabel->setText(title);
171     subTitleLabel->setText(subTitle);
172     subTitleLabel->setVisible(!subTitle.isEmpty());
173     explanationFrame->setVisible(!explanation.isEmpty());
174     explanationLabel->setVisible(!explanation.isEmpty());
175     explanationLabel->setText(explanation);
176     q->resize(q->sizeHint().expandedTo(q->size()));
177 }
178 
Wizard(QWidget * parent,Qt::WindowFlags f)179 Wizard::Wizard(QWidget *parent, Qt::WindowFlags f)
180     : QDialog(parent, f), d(new Private(this))
181 {
182 
183 }
184 
~Wizard()185 Wizard::~Wizard()
186 {
187     qCDebug(KLEOPATRA_LOG);
188 }
189 
setPage(int id,WizardPage * widget)190 void Wizard::setPage(int id, WizardPage *widget)
191 {
192     kleo_assert(id != InvalidPage);
193     kleo_assert(d->idToPage.find(id) == d->idToPage.end());
194     d->idToPage[id] = widget;
195     d->stack->addWidget(widget);
196     connect(widget, SIGNAL(completeChanged()), this, SLOT(updateButtonStates()));
197     connect(widget, SIGNAL(titleChanged()), this, SLOT(updateHeader()));
198     connect(widget, SIGNAL(subTitleChanged()), this, SLOT(updateHeader()));
199     connect(widget, SIGNAL(explanationChanged()), this, SLOT(updateHeader()));
200     connect(widget, SIGNAL(autoAdvanceChanged()), this, SLOT(updateButtonStates()));
201     connect(widget, SIGNAL(windowTitleChanged(QString)), this, SLOT(setWindowTitle(QString)));
202 }
203 
setPageOrder(const std::vector<int> & pageOrder)204 void Wizard::setPageOrder(const std::vector<int> &pageOrder)
205 {
206     d->pageOrder = pageOrder;
207     d->hiddenPages.clear();
208     if (pageOrder.empty()) {
209         return;
210     }
211     setCurrentPage(pageOrder.front());
212 }
213 
setCurrentPage(int id)214 void Wizard::setCurrentPage(int id)
215 {
216     d->currentId = id;
217     if (id == InvalidPage) {
218         return;
219     }
220     d->updateHeader();
221     d->updateButtonStates();
222 }
223 
setPageVisible(int id,bool visible)224 void Wizard::setPageVisible(int id, bool visible)
225 {
226     if (visible) {
227         d->hiddenPages.erase(id);
228     } else {
229         d->hiddenPages.insert(id);
230     }
231     if (currentPage() == id && !visible) {
232         next();
233     }
234 }
235 
currentPage() const236 int Wizard::currentPage() const
237 {
238     return d->currentId;
239 }
240 
canGoToNextPage() const241 bool Wizard::canGoToNextPage() const
242 {
243     const WizardPage *const current = currentPageWidget();
244     return current ? current->isComplete() : false;
245 }
246 
canGoToPreviousPage() const247 bool Wizard::canGoToPreviousPage() const
248 {
249     const int prev = d->previousPage();
250     if (prev == InvalidPage) {
251         return false;
252     }
253     const WizardPage *const prevPage = page(prev);
254     Q_ASSERT(prevPage);
255     return !prevPage->isCommitPage();
256 }
257 
next()258 void Wizard::next()
259 {
260     WizardPage *const current = currentPageWidget();
261     if (current) {
262         current->onNext();
263     }
264     onNext(d->currentId);
265     auto it = Kleo::binary_find(d->pageOrder.begin(), d->pageOrder.end(), d->currentId);
266     Q_ASSERT(it != d->pageOrder.end());
267 
268     do {
269         ++it;
270     } while (d->hiddenPages.find(*it) != d->hiddenPages.end());
271 
272     if (it == d->pageOrder.end()) { // "Finish"
273         d->currentId = InvalidPage;
274         close();
275     } else { // "next"
276         setCurrentPage(*it);
277     }
278 }
279 
previousPage() const280 int Wizard::Private::previousPage() const
281 {
282     if (pageOrder.empty()) {
283         return InvalidPage;
284     }
285 
286     auto it = Kleo::binary_find(pageOrder.begin(), pageOrder.end(), currentId);
287     if (it == pageOrder.begin() || it == pageOrder.end()) {
288         return InvalidPage;
289     }
290 
291     do {
292         --it;
293     } while (it != pageOrder.begin() && hiddenPages.find(*it) != hiddenPages.end());
294     return *it;
295 }
296 
back()297 void Wizard::back()
298 {
299     onBack(d->currentId);
300     const int prev = d->previousPage();
301     if (prev == InvalidPage) {
302         return;
303     }
304     setCurrentPage(prev);
305 }
306 
page(int id) const307 const WizardPage *Wizard::page(int id) const
308 {
309     if (id == InvalidPage) {
310         return nullptr;
311     }
312 
313     const auto it = d->idToPage.find(id);
314     kleo_assert(it != d->idToPage.end());
315     return (*it).second;
316 }
317 
currentPageWidget() const318 const WizardPage *Wizard::currentPageWidget() const
319 {
320     return page(d->currentId);
321 }
322 
currentPageWidget()323 WizardPage *Wizard::currentPageWidget()
324 {
325     return page(d->currentId);
326 }
327 
onNext(int currentId)328 void Wizard::onNext(int currentId)
329 {
330     Q_UNUSED(currentId)
331 }
332 
onBack(int currentId)333 void Wizard::onBack(int currentId)
334 {
335     Q_UNUSED(currentId)
336 }
337 
page(int id)338 WizardPage *Wizard::page(int id)
339 {
340     if (id == InvalidPage) {
341         return nullptr;
342     }
343 
344     const auto it = d->idToPage.find(id);
345     kleo_assert(it != d->idToPage.end());
346     return (*it).second;
347 }
348 
349 #include "moc_wizard.cpp"
350