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