modaloverlay.cpp raw
1 // Copyright (c) 2016-2022 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <limenka-build-config.h> // IWYU pragma: keep
6
7 #include <qt/modaloverlay.h>
8 #include <qt/forms/ui_modaloverlay.h>
9
10 #include <chainparams.h>
11 #include <qt/guiutil.h>
12 #include <qt/platformstyle.h>
13
14 #include <QColor>
15 #include <QEasingCurve>
16 #include <QPalette>
17 #include <QPropertyAnimation>
18 #include <QResizeEvent>
19
20 ModalOverlay::ModalOverlay(bool enable_wallet, const PlatformStyle& platform_style, QWidget* parent)
21 : QWidget(parent),
22 ui(new Ui::ModalOverlay),
23 bestHeaderDate(QDateTime()),
24 m_platform_style(platform_style)
25 {
26 ui->setupUi(this);
27
28 // Add ThemedLabel for warning icon
29 GUIUtil::ThemedLabel* warningIcon = new GUIUtil::ThemedLabel(&m_platform_style, this);
30 warningIcon->setThemedPixmap(QStringLiteral(":/icons/warning"), 48, 48);
31 ui->verticalLayoutIcon->insertWidget(0, warningIcon);
32
33 connect(ui->closeButton, &QPushButton::clicked, this, &ModalOverlay::closeClicked);
34 if (parent) {
35 parent->installEventFilter(this);
36 raise();
37 }
38 ui->closeButton->installEventFilter(this);
39
40 blockProcessTime.clear();
41 setVisible(false);
42 if (!enable_wallet) {
43 ui->infoText->setVisible(false);
44 ui->infoTextStrong->setText(tr("%1 is currently syncing. It will download headers and blocks from peers and validate them until reaching the tip of the block chain.").arg(CLIENT_NAME));
45 }
46
47 m_animation.setTargetObject(this);
48 m_animation.setPropertyName("pos");
49 m_animation.setDuration(300 /* ms */);
50 m_animation.setEasingCurve(QEasingCurve::OutQuad);
51
52 updateThemeStyles();
53 }
54
55 ModalOverlay::~ModalOverlay()
56 {
57 delete ui;
58 }
59
60 bool ModalOverlay::eventFilter(QObject * obj, QEvent * ev) {
61 if (obj == parent()) {
62 if (ev->type() == QEvent::Resize) {
63 QResizeEvent * rev = static_cast<QResizeEvent*>(ev);
64 resize(rev->size());
65 if (!layerIsVisible)
66 setGeometry(0, height(), width(), height());
67
68 if (m_animation.endValue().toPoint().y() > 0) {
69 m_animation.setEndValue(QPoint(0, height()));
70 }
71 }
72 else if (ev->type() == QEvent::ChildAdded) {
73 raise();
74 }
75 }
76
77 if (obj == ui->closeButton && ev->type() == QEvent::FocusOut && layerIsVisible) {
78 ui->closeButton->setFocus(Qt::OtherFocusReason);
79 }
80
81 return QWidget::eventFilter(obj, ev);
82 }
83
84 //! Tracks parent widget changes
85 bool ModalOverlay::event(QEvent* ev) {
86 if (ev->type() == QEvent::ParentAboutToChange) {
87 if (parent()) parent()->removeEventFilter(this);
88 }
89 else if (ev->type() == QEvent::ParentChange) {
90 if (parent()) {
91 parent()->installEventFilter(this);
92 raise();
93 }
94 }
95 return QWidget::event(ev);
96 }
97
98 void ModalOverlay::setKnownBestHeight(int count, const QDateTime& blockDate, bool presync)
99 {
100 if (!presync && count > bestHeaderHeight) {
101 bestHeaderHeight = count;
102 bestHeaderDate = blockDate;
103 UpdateHeaderSyncLabel();
104 }
105 if (presync) {
106 UpdateHeaderPresyncLabel(count, blockDate);
107 }
108 }
109
110 void ModalOverlay::tipUpdate(int count, const QDateTime& blockDate, double nVerificationProgress)
111 {
112 QDateTime currentDate = QDateTime::currentDateTime();
113
114 // keep a vector of samples of verification progress at height
115 blockProcessTime.push_front(qMakePair(currentDate.toMSecsSinceEpoch(), nVerificationProgress));
116
117 // show progress speed if we have more than one sample
118 if (blockProcessTime.size() >= 2) {
119 double progressDelta = 0;
120 double progressPerHour = 0;
121 qint64 timeDelta = 0;
122 qint64 remainingMSecs = 0;
123 double remainingProgress = 1.0 - nVerificationProgress;
124 for (int i = 1; i < blockProcessTime.size(); i++) {
125 QPair<qint64, double> sample = blockProcessTime[i];
126
127 // take first sample after 500 seconds or last available one
128 if (sample.first < (currentDate.toMSecsSinceEpoch() - 500 * 1000) || i == blockProcessTime.size() - 1) {
129 progressDelta = blockProcessTime[0].second - sample.second;
130 timeDelta = blockProcessTime[0].first - sample.first;
131 progressPerHour = (progressDelta > 0) ? progressDelta / (double)timeDelta * 1000 * 3600 : 0;
132 remainingMSecs = (progressDelta > 0) ? remainingProgress / progressDelta * timeDelta : -1;
133 break;
134 }
135 }
136 // show progress increase per hour
137 ui->progressIncreasePerH->setText(QString::number(progressPerHour * 100, 'f', 2)+"%");
138
139 // show expected remaining time
140 if(remainingMSecs >= 0) {
141 ui->expectedTimeLeft->setText(GUIUtil::formatNiceTimeOffset(remainingMSecs / 1000.0));
142 } else {
143 ui->expectedTimeLeft->setText(QObject::tr("unknown"));
144 }
145
146 static const int MAX_SAMPLES = 5000;
147 if (blockProcessTime.count() > MAX_SAMPLES) {
148 blockProcessTime.remove(MAX_SAMPLES, blockProcessTime.count() - MAX_SAMPLES);
149 }
150 }
151
152 // show the last block date
153 ui->newestBlockDate->setText(blockDate.toString());
154
155 // show the percentage done according to nVerificationProgress
156 ui->percentageProgress->setText(QString::number(nVerificationProgress*100, 'f', 2)+"%");
157
158 if (!bestHeaderDate.isValid())
159 // not syncing
160 return;
161
162 // estimate the number of headers left based on nPowTargetSpacing
163 // and check if the gui is not aware of the best header (happens rarely)
164 int estimateNumHeadersLeft = bestHeaderDate.secsTo(currentDate) / Params().GetConsensus().nPowTargetSpacing;
165 bool hasBestHeader = bestHeaderHeight >= count;
166
167 // show remaining number of blocks
168 if (estimateNumHeadersLeft < HEADER_HEIGHT_DELTA_SYNC && hasBestHeader) {
169 ui->numberOfBlocksLeft->setText(QString::number(bestHeaderHeight - count));
170 } else {
171 UpdateHeaderSyncLabel();
172 ui->expectedTimeLeft->setText(tr("Unknown…"));
173 }
174 }
175
176 void ModalOverlay::UpdateHeaderSyncLabel() {
177 int est_headers_left = bestHeaderDate.secsTo(QDateTime::currentDateTime()) / Params().GetConsensus().nPowTargetSpacing;
178 const int pct = bestHeaderHeight ? static_cast<int>(1000LL * bestHeaderHeight / (bestHeaderHeight + est_headers_left)) : 0;
179 ui->numberOfBlocksLeft->setText(tr("Unknown. Syncing Headers (%1, %2%)…").arg(bestHeaderHeight).arg(QStringLiteral("%1.%2").arg(pct / 10).arg(pct % 10)));
180 }
181
182 void ModalOverlay::UpdateHeaderPresyncLabel(int height, const QDateTime& blockDate) {
183 int est_headers_left = blockDate.secsTo(QDateTime::currentDateTime()) / Params().GetConsensus().nPowTargetSpacing;
184 const int pct = height ? static_cast<int>(1000LL * height / (height + est_headers_left)) : 0;
185 ui->numberOfBlocksLeft->setText(tr("Unknown. Pre-syncing Headers (%1, %2%)…").arg(height).arg(QStringLiteral("%1.%2").arg(pct / 10).arg(pct % 10)));
186 }
187
188 void ModalOverlay::toggleVisibility()
189 {
190 showHide(layerIsVisible, true);
191 if (!layerIsVisible)
192 userClosed = true;
193 }
194
195 void ModalOverlay::showHide(bool hide, bool userRequested)
196 {
197 if ( (layerIsVisible && !hide) || (!layerIsVisible && hide) || (!hide && userClosed && !userRequested))
198 return;
199
200 Q_EMIT triggered(hide);
201
202 if (!isVisible() && !hide)
203 setVisible(true);
204
205 m_animation.setStartValue(QPoint(0, hide ? 0 : height()));
206 // The eventFilter() updates the endValue if it is required for QEvent::Resize.
207 m_animation.setEndValue(QPoint(0, hide ? height() : 0));
208 m_animation.start(QAbstractAnimation::KeepWhenStopped);
209 layerIsVisible = !hide;
210
211 if (layerIsVisible) {
212 ui->closeButton->setFocus(Qt::OtherFocusReason);
213 }
214 }
215
216 void ModalOverlay::closeClicked()
217 {
218 showHide(true);
219 userClosed = true;
220 }
221
222 void ModalOverlay::changeEvent(QEvent* e)
223 {
224 if (e->type() == QEvent::PaletteChange) {
225 updateThemeStyles();
226 }
227 QWidget::changeEvent(e);
228 }
229
230 void ModalOverlay::updateThemeStyles()
231 {
232 const QColor bg_colour = palette().color(backgroundRole());
233 const bool dark_mode = GUIUtil::isDarkMode(bg_colour);
234
235 // Overlay background - both light and dark mode use black but with different alpha
236 QColor overlayBg = dark_mode ? QColor(0, 0, 0, 240) : QColor(0, 0, 0, 220);
237 const QString bgStyle = QStringLiteral("#bgWidget { background: ") + overlayBg.name(QColor::HexArgb) + QStringLiteral("; }");
238 ui->bgWidget->setStyleSheet(bgStyle);
239
240 // Content widget with system window background, slightly transparent
241 QColor contentBg = QColor(bg_colour.red(), bg_colour.green(), bg_colour.blue(), 240);
242 const QString contentStyle = QStringLiteral("#contentWidget { background: ") + contentBg.name(QColor::HexArgb) + QStringLiteral("; border-radius: 6px; }");
243 ui->contentWidget->setStyleSheet(contentStyle);
244 }
245