1 #include "filezilla.h"
2 #include "queue.h"
3 #include "statuslinectrl.h"
4 #include <wx/dcbuffer.h>
5 #include "Options.h"
6 #include "sizeformatting.h"
7 #include "themeprovider.h"
8
9 #include <algorithm>
10
11 BEGIN_EVENT_TABLE(CStatusLineCtrl, wxWindow)
12 EVT_PAINT(CStatusLineCtrl::OnPaint)
13 EVT_TIMER(wxID_ANY, CStatusLineCtrl::OnTimer)
14 EVT_ERASE_BACKGROUND(CStatusLineCtrl::OnEraseBackground)
15 END_EVENT_TABLE()
16
17 int CStatusLineCtrl::m_fieldOffsets[4];
18 wxCoord CStatusLineCtrl::m_textHeight;
19 bool CStatusLineCtrl::m_initialized = false;
20 int CStatusLineCtrl::m_barWidth = 102;
21
CStatusLineCtrl(CQueueView * pParent,const t_EngineData * const pEngineData,const wxRect & initialPosition)22 CStatusLineCtrl::CStatusLineCtrl(CQueueView* pParent, const t_EngineData* const pEngineData, const wxRect& initialPosition)
23 : m_pParent(pParent)
24 , m_pEngineData(pEngineData)
25 {
26 wxASSERT(pEngineData);
27
28 Create(pParent->GetMainWindow(), wxID_ANY, initialPosition.GetPosition(), initialPosition.GetSize());
29
30 SetOwnFont(pParent->GetFont());
31 SetForegroundColour(pParent->GetForegroundColour());
32 SetBackgroundStyle(wxBG_STYLE_CUSTOM);
33 SetBackgroundColour(pParent->GetBackgroundColour());
34
35 m_transferStatusTimer.SetOwner(this);
36
37 InitFieldOffsets();
38
39 ClearTransferStatus();
40 }
41
InitFieldOffsets()42 void CStatusLineCtrl::InitFieldOffsets()
43 {
44 if (m_initialized) {
45 return;
46 }
47 m_initialized = true;
48
49 // Calculate field widths so that the contents fit under every language.
50 wxClientDC dc(this);
51 dc.SetFont(GetFont());
52
53 double scale = CThemeProvider::GetUIScaleFactor();
54 m_barWidth *= scale;
55
56 wxCoord w, h;
57 wxTimeSpan elapsed(100, 0, 0);
58 // @translator: This is a date/time formatting specifier. See https://wiki.filezilla-project.org/Date_and_Time_formatting
59 dc.GetTextExtent(elapsed.Format(_("%H:%M:%S elapsed")), &w, &h);
60 m_textHeight = h;
61 m_fieldOffsets[0] = scale * 50 + w;
62
63 // @translator: This is a date/time formatting specifier. See https://wiki.filezilla-project.org/Date_and_Time_formatting
64 dc.GetTextExtent(elapsed.Format(_("%H:%M:%S left")), &w, &h);
65 m_fieldOffsets[1] = m_fieldOffsets[0] + scale * 20 + w;
66
67 m_fieldOffsets[2] = m_fieldOffsets[1] + scale * 20;
68 m_fieldOffsets[3] = m_fieldOffsets[2] + scale * 20 + m_barWidth;
69 }
70
~CStatusLineCtrl()71 CStatusLineCtrl::~CStatusLineCtrl()
72 {
73 if (!status_.empty() && status_.totalSize >= 0) {
74 if (m_pEngineData && m_pEngineData->pItem) {
75 m_pEngineData->pItem->SetSize(status_.totalSize);
76 }
77 }
78
79 if (m_transferStatusTimer.IsRunning()) {
80 m_transferStatusTimer.Stop();
81 }
82 }
83
OnPaint(wxPaintEvent &)84 void CStatusLineCtrl::OnPaint(wxPaintEvent&)
85 {
86 wxPaintDC dc(this);
87
88 wxRect rect = GetRect();
89
90 int refresh = 0;
91 if (!m_data.IsOk() || rect.GetWidth() != m_data.GetWidth() || rect.GetHeight() != m_data.GetHeight()) {
92 m_mdc.reset();
93
94 double sf = dc.GetContentScaleFactor();
95 m_data.CreateScaled(rect.width, rect.height, -1, sf);
96
97 m_mdc = std::make_unique<wxMemoryDC>(m_data);
98 // Use same layout direction as the DC which bitmap is drawn on.
99 // This avoids problem with mirrored characters on RTL locales.
100 m_mdc->SetLayoutDirection(dc.GetLayoutDirection());
101
102 refresh = 31;
103 }
104
105 fz::duration elapsed;
106 int left = -1;
107 wxFileOffset rate;
108 wxString bytes_and_rate;
109 int bar_split = -1;
110 int permill = -1;
111
112 if (status_.empty()) {
113 if (m_previousStatusText != m_statusText) {
114 // Clear background
115 m_mdc->SetFont(GetFont());
116 m_mdc->SetPen(GetBackgroundColour());
117 m_mdc->SetBrush(GetBackgroundColour());
118 m_mdc->SetTextForeground(GetForegroundColour());
119 m_mdc->DrawRectangle(0, 0, rect.GetWidth(), rect.GetHeight());
120 wxCoord h = (rect.GetHeight() - m_textHeight) / 2;
121 m_mdc->DrawText(m_statusText, 50, h);
122 m_previousStatusText = m_statusText;
123 refresh = 0;
124 }
125 }
126 else {
127 if (!m_previousStatusText.empty()) {
128 m_previousStatusText.clear();
129 refresh = 31;
130 }
131
132 int elapsed_milli_seconds = 0;
133 if (!status_.started.empty()) {
134 elapsed = fz::datetime::now() - status_.started;
135 elapsed_milli_seconds = static_cast<int>(elapsed.get_milliseconds()); // Assume it doesn't overflow
136 }
137
138 if (elapsed_milli_seconds / 1000 != m_last_elapsed_seconds) {
139 refresh |= 1;
140 m_last_elapsed_seconds = elapsed_milli_seconds / 1000;
141 }
142
143 if (COptions::Get()->get_int(OPTION_SPEED_DISPLAY)) {
144 rate = GetMomentarySpeed();
145 }
146 else {
147 rate = GetAverageSpeed(elapsed_milli_seconds);
148 }
149
150 if (status_.totalSize > 0 && elapsed_milli_seconds >= 1000 && rate > 0) {
151 wxFileOffset r = status_.totalSize - status_.currentOffset;
152 left = r / rate + 1;
153 if (r) {
154 ++left;
155 }
156
157 if (left < 0) {
158 left = 0;
159 }
160 }
161
162 if (m_last_left != left) {
163 refresh |= 2;
164 m_last_left = left;
165 }
166
167 const wxString bytestr = CSizeFormat::Format(status_.currentOffset, true, CSizeFormat::bytes, COptions::Get()->get_int(OPTION_SIZE_USETHOUSANDSEP) != 0, 0);
168 if (elapsed_milli_seconds >= 1000 && rate > -1) {
169 CSizeFormat::_format format = static_cast<CSizeFormat::_format>(COptions::Get()->get_int(OPTION_SIZE_FORMAT));
170 if (format == CSizeFormat::bytes) {
171 format = CSizeFormat::iec;
172 }
173 const wxString ratestr = CSizeFormat::Format(rate, true,
174 format,
175 COptions::Get()->get_int(OPTION_SIZE_USETHOUSANDSEP) != 0,
176 COptions::Get()->get_int(OPTION_SIZE_DECIMALPLACES));
177 bytes_and_rate.Printf(_("%s (%s/s)"), bytestr, ratestr );
178 }
179 else {
180 bytes_and_rate.Printf(_("%s (? B/s)"), bytestr);
181 }
182
183 if (m_last_bytes_and_rate != bytes_and_rate) {
184 refresh |= 8;
185 m_last_bytes_and_rate = bytes_and_rate;
186 }
187
188 if (status_.totalSize > 0) {
189 bar_split = static_cast<int>(status_.currentOffset * (m_barWidth - 2) / status_.totalSize);
190 if (bar_split > (m_barWidth - 2)) {
191 bar_split = m_barWidth - 2;
192 }
193
194 if (status_.currentOffset > status_.totalSize) {
195 permill = 1001;
196 }
197 else {
198 permill = static_cast<int>(status_.currentOffset * 1000 / status_.totalSize);
199 }
200 }
201
202 if (m_last_bar_split != bar_split || m_last_permill != permill) {
203 refresh |= 4;
204 m_last_bar_split = bar_split;
205 m_last_permill = permill;
206 }
207 }
208
209 if (refresh) {
210 m_mdc->SetFont(GetFont());
211 m_mdc->SetPen(GetBackgroundColour());
212 m_mdc->SetBrush(GetBackgroundColour());
213 m_mdc->SetTextForeground(GetForegroundColour());
214
215 // Get character height so that we can center the text vertically.
216 wxCoord h = (rect.GetHeight() - m_textHeight) / 2;
217
218 if (refresh & 1) {
219 m_mdc->DrawRectangle(0, 0, m_fieldOffsets[0], rect.GetHeight() + 1);
220 DrawRightAlignedText(*m_mdc, wxTimeSpan::Milliseconds(elapsed.get_milliseconds()).Format(_("%H:%M:%S elapsed")), m_fieldOffsets[0], h);
221 }
222 if (refresh & 2) {
223 m_mdc->DrawRectangle(m_fieldOffsets[0], 0, m_fieldOffsets[1] - m_fieldOffsets[0], rect.GetHeight() + 1);
224 if (left != -1) {
225 wxTimeSpan timeLeft(0, 0, left);
226 DrawRightAlignedText(*m_mdc, timeLeft.Format(_("%H:%M:%S left")), m_fieldOffsets[1], h);
227 }
228 else {
229 DrawRightAlignedText(*m_mdc, _("--:--:-- left"), m_fieldOffsets[1], h);
230 }
231 }
232 if (refresh & 8) {
233 m_mdc->DrawRectangle(m_fieldOffsets[3], 0, rect.GetWidth() - m_fieldOffsets[3], rect.GetHeight() + 1);
234 m_mdc->DrawText(bytes_and_rate, m_fieldOffsets[3], h);
235 }
236 if (refresh & 16) {
237 m_mdc->DrawRectangle(m_fieldOffsets[1], 0, m_fieldOffsets[2] - m_fieldOffsets[1], rect.GetHeight() + 1);
238 }
239 if (refresh & 4) {
240 m_mdc->DrawRectangle(m_fieldOffsets[2], 0, m_fieldOffsets[3] - m_fieldOffsets[2], rect.GetHeight() + 1);
241 if (bar_split != -1) {
242 DrawProgressBar(*m_mdc, m_fieldOffsets[2], 1, rect.GetHeight() - 2, bar_split, permill);
243 }
244 }
245 }
246 dc.Blit(0, 0, rect.GetWidth(), rect.GetHeight(), m_mdc.get(), 0, 0);
247 }
248
ClearTransferStatus()249 void CStatusLineCtrl::ClearTransferStatus()
250 {
251 if (!status_.empty() && status_.totalSize >= 0) {
252 if (m_pEngineData && m_pEngineData->pItem) {
253 m_pParent->UpdateItemSize(m_pEngineData->pItem, status_.totalSize);
254 }
255 }
256 status_.clear();
257
258 auto const state = m_pEngineData ? m_pEngineData->state : t_EngineData::none;
259 switch (state)
260 {
261 case t_EngineData::disconnect:
262 m_statusText = _("Disconnecting from previous server");
263 break;
264 case t_EngineData::cancel:
265 m_statusText = _("Waiting for transfer to be cancelled");
266 break;
267 case t_EngineData::connect:
268 m_statusText = wxString::Format(_("Connecting to %s"), m_pEngineData->lastSite.Format(ServerFormat::with_user_and_optional_port));
269 break;
270 default:
271 m_statusText = _("Transferring");
272 break;
273 }
274
275 if (m_transferStatusTimer.IsRunning()) {
276 m_transferStatusTimer.Stop();
277 }
278
279 m_past_data_count = 0;
280
281 m_monentary_speed_data = monentary_speed_data();
282 Refresh(false);
283 }
284
SetTransferStatus(CTransferStatus const & status)285 void CStatusLineCtrl::SetTransferStatus(CTransferStatus const& status)
286 {
287 if (!status) {
288 ClearTransferStatus();
289 }
290 else {
291 status_ = status;
292
293 m_lastOffset = status.currentOffset;
294
295 if (!m_transferStatusTimer.IsRunning()) {
296 m_transferStatusTimer.Start(100);
297 }
298 Refresh(false);
299 }
300 }
301
OnTimer(wxTimerEvent &)302 void CStatusLineCtrl::OnTimer(wxTimerEvent&)
303 {
304 if (!m_pEngineData || !m_pEngineData->pEngine) {
305 m_transferStatusTimer.Stop();
306 return;
307 }
308
309 bool changed;
310 CTransferStatus status = m_pEngineData->pEngine->GetTransferStatus(changed);
311
312 if (status.empty()) {
313 ClearTransferStatus();
314 }
315 else if (changed) {
316 if (status.madeProgress && !status.list &&
317 m_pEngineData->pItem->GetType() == QueueItemType::File)
318 {
319 CFileItem* pItem = (CFileItem*)m_pEngineData->pItem;
320 pItem->set_made_progress(true);
321 }
322 SetTransferStatus(status);
323 }
324 else {
325 m_transferStatusTimer.Stop();
326 }
327 }
328
DrawRightAlignedText(wxDC & dc,wxString const & text,int x,int y)329 void CStatusLineCtrl::DrawRightAlignedText(wxDC& dc, wxString const& text, int x, int y)
330 {
331 wxCoord w, h;
332 dc.GetTextExtent(text, &w, &h);
333 x -= w;
334
335 dc.DrawText(text, x, y);
336 }
337
OnEraseBackground(wxEraseEvent &)338 void CStatusLineCtrl::OnEraseBackground(wxEraseEvent&)
339 {
340 // Don't erase background, only causes status line to flicker.
341 }
342
DrawProgressBar(wxDC & dc,int x,int y,int height,int bar_split,int permill)343 void CStatusLineCtrl::DrawProgressBar(wxDC& dc, int x, int y, int height, int bar_split, int permill)
344 {
345 wxASSERT(bar_split != -1);
346 wxASSERT(permill != -1);
347
348 // Draw right part
349 dc.SetPen(*wxTRANSPARENT_PEN);
350 dc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
351 dc.DrawRectangle(x + 1 + bar_split, y + 1, m_barWidth - bar_split - 1, height - 2);
352
353 if (bar_split && height > 2) {
354 // Draw pretty gradient
355
356 int greenmin = 160;
357 int greenmax = 223;
358 int colourCount = ((height + 1) / 2);
359
360 for (int i = 0; i < colourCount; ++i) {
361 int curGreen = greenmax - ((greenmax - greenmin) * i / (colourCount - 1));
362 dc.SetPen(wxPen(wxColour(0, curGreen, 0)));
363 dc.DrawLine(x + 1, y + colourCount - i, x + 1 + bar_split, y + colourCount - i);
364 dc.DrawLine(x + 1, y + height - colourCount + i - 1, x + 1 + bar_split, y + height - colourCount + i - 1);
365 }
366 }
367
368 dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW));
369 dc.SetBrush(*wxTRANSPARENT_BRUSH);
370 dc.DrawRectangle(x, y, m_barWidth, height);
371
372 // Draw percentage-done text
373 wxString text;
374 if (permill > 1000) {
375 text = _T("> 100.0%");
376 }
377 else {
378 text = wxString::Format(_T("%d.%d%%"), permill / 10, permill % 10);
379 }
380
381 wxCoord w, h;
382 dc.GetTextExtent(text, &w, &h);
383 dc.DrawText(text, x + m_barWidth / 2 - w / 2, y + height / 2 - h / 2);
384 }
385
GetAverageSpeed(int elapsed_milli_seconds)386 wxFileOffset CStatusLineCtrl::GetAverageSpeed(int elapsed_milli_seconds)
387 {
388 if (status_.empty()) {
389 return -1;
390 }
391
392 if (elapsed_milli_seconds <= 0) {
393 return -1;
394 }
395
396 int elapsed_seconds = elapsed_milli_seconds / 1000;
397 while (m_past_data_count < 10 && elapsed_seconds > m_past_data_count) {
398 m_past_data[m_past_data_count].elapsed = elapsed_milli_seconds;
399 m_past_data[m_past_data_count].offset = status_.currentOffset - status_.startOffset;
400 ++m_past_data_count;
401 }
402
403 _past_data forget;
404
405 int offset = (elapsed_seconds - 1) / 2;
406 if (offset > 0) {
407 forget = m_past_data[std::min(offset, m_past_data_count - 1)];
408 }
409
410 if (elapsed_milli_seconds <= forget.elapsed) {
411 return -1;
412 }
413
414 return ((status_.currentOffset - status_.startOffset - forget.offset) * 1000) / (elapsed_milli_seconds - forget.elapsed);
415 }
416
GetMomentarySpeed()417 wxFileOffset CStatusLineCtrl::GetMomentarySpeed()
418 {
419 if (status_.empty()) {
420 return -1;
421 }
422
423 if (m_monentary_speed_data.last_offset < 0) {
424 m_monentary_speed_data.last_offset = status_.currentOffset;
425 }
426
427 if (!m_monentary_speed_data.last_update) {
428 m_monentary_speed_data.last_update = fz::monotonic_clock::now();
429 return -1;
430 }
431
432 fz::duration const time_diff = fz::monotonic_clock::now() - m_monentary_speed_data.last_update;
433 if (time_diff.get_seconds() >= 2) {
434 m_monentary_speed_data.last_update = fz::monotonic_clock::now();
435 }
436 else if (m_monentary_speed_data.last_speed >= 0 || !time_diff) {
437 return m_monentary_speed_data.last_speed;
438 }
439
440 wxFileOffset const fileOffsetDiff = status_.currentOffset - m_monentary_speed_data.last_offset;
441 m_monentary_speed_data.last_offset = status_.currentOffset;
442 if (fileOffsetDiff >= 0) {
443 m_monentary_speed_data.last_speed = fileOffsetDiff * 1000 / time_diff.get_milliseconds();
444 }
445
446 return m_monentary_speed_data.last_speed;
447 }
448
Show(bool show)449 bool CStatusLineCtrl::Show(bool show)
450 {
451 if (show) {
452 if (!m_transferStatusTimer.IsRunning()) {
453 m_transferStatusTimer.Start(100);
454 }
455 }
456 else {
457 m_transferStatusTimer.Stop();
458 }
459
460 return wxWindow::Show(show);
461 }
462
SetEngineData(const t_EngineData * const pEngineData)463 void CStatusLineCtrl::SetEngineData(const t_EngineData* const pEngineData)
464 {
465 m_pEngineData = pEngineData;
466 }
467