1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 2004-2009, RedWolf Design GmbH, http://www.clonk.de/
5  * Copyright (c) 2009-2016, The OpenClonk Team and contributors
6  *
7  * Distributed under the terms of the ISC license; see accompanying file
8  * "COPYING" for details.
9  *
10  * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11  * See accompanying file "TRADEMARK" for details.
12  *
13  * To redistribute this file separately, substitute the full license texts
14  * for the above references.
15  */
16 // dialogs for network information
17 
18 #include "C4Include.h"
19 #include "network/C4Network2Dialogs.h"
20 
21 #include "control/C4GameControl.h"
22 #include "game/C4Viewport.h"
23 #include "graphics/C4Draw.h"
24 #include "graphics/C4GraphicsResource.h"
25 #include "gui/C4GameOptions.h"
26 #include "gui/C4Startup.h"
27 #include "lib/StdColors.h"
28 #include "network/C4Network2.h"
29 #include "network/C4Network2Stats.h"
30 #include "player/C4PlayerList.h"
31 
32 #ifndef HAVE_WINSOCK
33 #include <sys/socket.h>
34 #include <netinet/in.h>
35 #include <arpa/inet.h>
36 #endif
37 
38 // --------------------------------------------------
39 // C4Network2ClientDlg
40 
C4Network2ClientDlg(int iForClientID)41 C4Network2ClientDlg::C4Network2ClientDlg(int iForClientID)
42 		: C4GUI::InfoDialog(LoadResStr("IDS_NET_CLIENT_INFO"), 10), iClientID(iForClientID)
43 {
44 	// initial text update
45 	UpdateText();
46 }
47 
UpdateText()48 void C4Network2ClientDlg::UpdateText()
49 {
50 	// begin updating (clears previous text)
51 	BeginUpdateText();
52 	// get core
53 	const C4Client *pClient = Game.Clients.getClientByID(iClientID);
54 	if (!pClient)
55 	{
56 		// client ID unknown
57 		AddLineFmt(LoadResStr("IDS_NET_CLIENT_INFO_UNKNOWNID"), iClientID);
58 	}
59 	else
60 	{
61 		// get client (may be nullptr for local info)
62 		C4Network2Client *pNetClient = pClient->getNetClient();
63 		// show some info
64 		StdCopyStrBuf strInfo;
65 		if (!pClient->isActivated()) { strInfo.Append(LoadResStr("IDS_MSG_INACTIVE")); strInfo.Append(" "); }
66 		if (pClient->isLocal()) { strInfo.Append(LoadResStr("IDS_MSG_LOCAL")); strInfo.Append(" "); }
67 		strInfo.AppendFormat("%s %s (ID #%d)%s",
68 			LoadResStr(pClient->isHost() ? "IDS_MSG_HOST" : "IDS_MSG_CLIENT"),
69 			pClient->getName(),
70 			iClientID,
71 			::Network.isHost() && pNetClient && !pNetClient->isReady() ? " (!ack)" : "");
72 		AddLine(strInfo.getData());
73 		// show addresses
74 		int iCnt;
75 		if ((iCnt=pNetClient->getAddrCnt()))
76 		{
77 			AddLine(LoadResStr("IDS_NET_CLIENT_INFO_ADDRESSES"));
78 			for (int i=0; i<iCnt; ++i)
79 			{
80 				C4Network2Address addr = pNetClient->getAddr(i);
81 				AddLineFmt("  %d: %s",
82 				           i,                        // adress index
83 				           addr.toString().getData());
84 			}
85 		}
86 		else
87 			AddLine(LoadResStr("IDS_NET_CLIENT_INFO_NOADDRESSES"));
88 		// show connection
89 		if (pNetClient)
90 		{
91 			// connections
92 			if (pNetClient->isConnected())
93 			{
94 				AddLineFmt(LoadResStr("IDS_NET_CLIENT_INFO_CONNECTIONS"),
95 				           pNetClient->getMsgConn() == pNetClient->getDataConn() ? "Msg/Data" : "Msg",
96 				           ::Network.NetIO.getNetIOName(pNetClient->getMsgConn()->getNetClass()),
97 						   pNetClient->getMsgConn()->getPeerAddr().ToString().getData(),
98 				           pNetClient->getMsgConn()->getPingTime());
99 				if (pNetClient->getMsgConn() != pNetClient->getDataConn())
100 					AddLineFmt(LoadResStr("IDS_NET_CLIENT_INFO_CONNDATA"),
101 					           ::Network.NetIO.getNetIOName(pNetClient->getDataConn()->getNetClass()),
102 					           pNetClient->getDataConn()->getPeerAddr().ToString().getData(),
103 					           pNetClient->getDataConn()->getPingTime());
104 			}
105 			else
106 				AddLine(LoadResStr("IDS_NET_CLIENT_INFO_NOCONNECTIONS"));
107 		}
108 	}
109 	// update done
110 	EndUpdateText();
111 }
112 
113 
114 // --------------------------------------------------
115 // C4Network2ClientListBox::ClientListItem
116 
ClientListItem(class C4Network2ClientListBox * pForDlg,int iClientID)117 C4Network2ClientListBox::ClientListItem::ClientListItem(class C4Network2ClientListBox *pForDlg, int iClientID) // ctor
118 		: ListItem(pForDlg, iClientID), pStatusIcon(nullptr), pName(nullptr), pPing(nullptr), pActivateBtn(nullptr), pKickBtn(nullptr), last_sound_time(0)
119 {
120 	// get associated client
121 	const C4Client *pClient = GetClient();
122 	// get size
123 	int iIconSize = ::GraphicsResource.TextFont.GetLineHeight();
124 	if (pForDlg->IsStartup()) iIconSize *= 2;
125 	int iWidth = pForDlg->GetItemWidth();
126 	int iVerticalIndent = 2;
127 	SetBounds(C4Rect(0, 0, iWidth, iIconSize+2*iVerticalIndent));
128 	C4GUI::ComponentAligner ca(GetContainedClientRect(), 0,iVerticalIndent);
129 	// create subcomponents
130 	bool fIsHost = pClient && pClient->isHost();
131 	pStatusIcon = new C4GUI::Icon(ca.GetFromLeft(iIconSize), fIsHost ? C4GUI::Ico_Host : C4GUI::Ico_Client);
132 	StdStrBuf sNameLabel;
133 	if (pClient)
134 	{
135 		if (pForDlg->IsStartup())
136 			sNameLabel.Ref(pClient->getName());
137 		else
138 			sNameLabel.Format("%s:%s", pClient->getName(), pClient->getNick());
139 	}
140 	else
141 	{
142 		sNameLabel.Ref("???");
143 	}
144 	pName = new C4GUI::Label(sNameLabel.getData(), iIconSize + IconLabelSpacing,iVerticalIndent, ALeft);
145 	int iPingRightPos = GetBounds().Wdt - IconLabelSpacing;
146 	if (::Network.isHost()) iPingRightPos -= 48;
147 	if (::Network.isHost() && pClient && !pClient->isHost())
148 	{
149 		// activate/deactivate and kick btns for clients at host
150 		if (!pForDlg->IsStartup())
151 		{
152 			pActivateBtn = new C4GUI::CallbackButtonEx<C4Network2ClientListBox::ClientListItem, C4GUI::IconButton>(C4GUI::Ico_Active, GetToprightCornerRect(std::max(iIconSize, 16),std::max(iIconSize, 16),2,1,1), 0, this, &ClientListItem::OnButtonActivate);
153 			fShownActive = true;
154 		}
155 		pKickBtn = new  C4GUI::CallbackButtonEx<C4Network2ClientListBox::ClientListItem, C4GUI::IconButton>(C4GUI::Ico_Kick, GetToprightCornerRect(std::max(iIconSize, 16),std::max(iIconSize, 16),2,1,0), 0, this, &ClientListItem::OnButtonKick);
156 		pKickBtn->SetToolTip(LoadResStrNoAmp("IDS_NET_KICKCLIENT"));
157 	}
158 	if (!pForDlg->IsStartup()) if (pClient && !pClient->isLocal())
159 		{
160 			// wait time
161 			pPing = new C4GUI::Label("???", iPingRightPos, iVerticalIndent, ARight);
162 			pPing->SetToolTip(LoadResStr("IDS_DESC_CONTROLWAITTIME"));
163 		}
164 	// add components
165 	AddElement(pStatusIcon); AddElement(pName);
166 	if (pPing) AddElement(pPing);
167 	if (pActivateBtn) AddElement(pActivateBtn);
168 	if (pKickBtn) AddElement(pKickBtn);
169 	// add to listbox (will eventually get moved)
170 	pForDlg->AddElement(this);
171 	// first-time update
172 	Update();
173 }
174 
Update()175 void C4Network2ClientListBox::ClientListItem::Update()
176 {
177 	// update wait label
178 	if (pPing)
179 	{
180 		int iWait = ::Control.Network.ClientPerfStat(iClientID);
181 		pPing->SetText(FormatString("%d ms", iWait).getData());
182 		pPing->SetColor(C4RGB(
183 		                  Clamp(255-Abs(iWait)*5, 0, 255),
184 		                  Clamp(255-iWait*5, 0, 255),
185 		                  Clamp(255+iWait*5, 0, 255)));
186 	}
187 	// update activation status
188 	const C4Client *pClient = GetClient(); if (!pClient) return;
189 	bool fIsActive = pClient->isActivated();
190 	if (fIsActive != fShownActive)
191 	{
192 		fShownActive = fIsActive;
193 		if (!pClient->isHost()) pStatusIcon->SetIcon(fIsActive ? C4GUI::Ico_Client : C4GUI::Ico_ObserverClient);
194 		if (pActivateBtn)
195 		{
196 			pActivateBtn->SetIcon(fIsActive ? C4GUI::Ico_Active : C4GUI::Ico_Inactive);
197 			pActivateBtn->SetToolTip(LoadResStrNoAmp(fIsActive ? "IDS_NET_DEACTIVATECLIENT" : "IDS_NET_ACTIVATECLIENT"));
198 		}
199 	}
200 	// update players in tooltip
201 	StdStrBuf sCltPlrs(Game.PlayerInfos.GetActivePlayerNames(false, iClientID));
202 	pName->SetToolTip(sCltPlrs.getData());
203 	// update icon: Network status
204 	C4GUI::Icons icoStatus = C4GUI::Ico_UnknownClient;
205 	C4Network2Client *pClt = pClient->getNetClient();
206 	if (pClt)
207 	{
208 		switch (pClt->getStatus())
209 		{
210 		case NCS_Joining: // waiting for join data
211 		case NCS_Chasing: // client is behind (status not acknowledged, isn't waited for)
212 		case NCS_NotReady: // client is behind (status not acknowledged)
213 			icoStatus = C4GUI::Ico_Loading;
214 			break;
215 
216 		case NCS_Ready: // client acknowledged network status
217 			icoStatus = C4GUI::Ico_Ready;
218 			break;
219 
220 		case NCS_Remove: // client is to be removed
221 			icoStatus = C4GUI::Ico_Kick;
222 			break;
223 
224 		default: // whatever
225 			assert(false);
226 			icoStatus = C4GUI::Ico_Loading;
227 			break;
228 		}
229 	}
230 	// sound icon?
231 	if (last_sound_time)
232 	{
233 		time_t dt = time(nullptr) - last_sound_time;
234 		if (dt >= SoundIconShowTime)
235 		{
236 			// stop showing sound icon
237 			last_sound_time = 0;
238 		}
239 		else
240 		{
241 			// time not up yet: show sound icon
242 			icoStatus = C4GUI::Ico_Sound;
243 		}
244 	}
245 	// network OK - control ready?
246 	if (!pForDlg->IsStartup() && (icoStatus == C4GUI::Ico_Ready))
247 	{
248 		if (!::Control.Network.ClientReady(iClientID, ::Control.ControlTick))
249 		{
250 			// control not ready
251 			icoStatus = C4GUI::Ico_NetWait;
252 		}
253 	}
254 	// set new icon
255 	pStatusIcon->SetIcon(icoStatus);
256 }
257 
GetClient() const258 const C4Client *C4Network2ClientListBox::ClientListItem::GetClient() const
259 {
260 	return Game.Clients.getClientByID(iClientID);
261 }
262 
GetClientListItem(int32_t client_id)263 C4Network2ClientListBox::ClientListItem *C4Network2ClientListBox::GetClientListItem(int32_t client_id)
264 {
265 	// find list item that is not the connection item
266 	// search through listbox
267 	for (C4GUI::Element *list_item = GetFirst(); list_item; list_item = list_item->GetNext())
268 	{
269 		// only playerlistitems in this box
270 		ListItem *list_item2 = static_cast<ListItem *>(list_item);
271 		if (list_item2->GetClientID() == client_id)
272 			if (list_item2->GetConnectionID() == -1)
273 				return static_cast<ClientListItem *>(list_item2);
274 	}
275 	// nothing found
276 	return nullptr;
277 }
278 
OnButtonActivate(C4GUI::Control * pButton)279 void C4Network2ClientListBox::ClientListItem::OnButtonActivate(C4GUI::Control *pButton)
280 {
281 	// league: Do not deactivate clients with players
282 	if (Game.Parameters.isLeague() && ::Players.GetAtClient(iClientID))
283 	{
284 		Log(LoadResStr("IDS_LOG_COMMANDNOTALLOWEDINLEAGUE"));
285 		return;
286 	}
287 	// change to status that is not currently shown
288 	::Control.DoInput(CID_ClientUpdate, new C4ControlClientUpdate(iClientID, CUT_Activate, !fShownActive), CDT_Sync);
289 }
290 
OnButtonKick(C4GUI::Control * pButton)291 void C4Network2ClientListBox::ClientListItem::OnButtonKick(C4GUI::Control *pButton)
292 {
293 	// try kick
294 	// league: Kick needs voting
295 	if (Game.Parameters.isLeague() && ::Players.GetAtClient(iClientID))
296 		::Network.Vote(VT_Kick, true, iClientID);
297 	else
298 		Game.Clients.CtrlRemove(GetClient(), LoadResStr(pForDlg->IsStartup() ? "IDS_MSG_KICKFROMSTARTUPDLG" : "IDS_MSG_KICKFROMCLIENTLIST"));
299 }
300 
SetSoundIcon()301 void C4Network2ClientListBox::ClientListItem::SetSoundIcon()
302 {
303 	// remember time for reset
304 	last_sound_time = time(nullptr);
305 	// force icon
306 	Update();
307 }
308 
309 
310 // --------------------------------------------------
311 // C4Network2ClientListBox::ConnectionListItem
312 
ConnectionListItem(class C4Network2ClientListBox * pForDlg,int32_t iClientID,int32_t iConnectionID)313 C4Network2ClientListBox::ConnectionListItem::ConnectionListItem(class C4Network2ClientListBox *pForDlg, int32_t iClientID, int32_t iConnectionID) // ctor
314 		: ListItem(pForDlg, iClientID), iConnID(iConnectionID), pDesc(nullptr), pPing(nullptr), pReconnectBtn(nullptr), pDisconnectBtn(nullptr)
315 {
316 	// get size
317 	CStdFont &rUseFont = ::GraphicsResource.TextFont;
318 	int iIconSize = rUseFont.GetLineHeight();
319 	int iWidth = pForDlg->GetItemWidth();
320 	int iVerticalIndent = 2;
321 	SetBounds(C4Rect(0, 0, iWidth, iIconSize+2*iVerticalIndent));
322 	C4GUI::ComponentAligner ca(GetContainedClientRect(), 0,iVerticalIndent);
323 	// left indent
324 	ca.ExpandLeft(-iIconSize*2);
325 	// create subcomponents
326 	// reconnect/disconnect buttons
327 	if (!Game.Parameters.isLeague())
328 	{
329 		pDisconnectBtn = new  C4GUI::CallbackButtonEx<C4Network2ClientListBox::ConnectionListItem, C4GUI::IconButton>(C4GUI::Ico_Disconnect, ca.GetFromRight(iIconSize, iIconSize), 0, this, &ConnectionListItem::OnButtonDisconnect);
330 		pDisconnectBtn->SetToolTip(LoadResStr("IDS_MENU_DISCONNECT"));
331 	}
332 	else
333 		pDisconnectBtn = nullptr;
334 	// ping time
335 	int32_t sx=40, sy=iIconSize;
336 	rUseFont.GetTextExtent("???? ms", sx,sy, true);
337 	pPing = new C4GUI::Label("???", ca.GetFromRight(sx, sy), ARight);
338 	pPing->SetToolTip(LoadResStr("IDS_NET_CONTROL_PING"));
339 	// main description item
340 	pDesc = new C4GUI::Label("???", ca.GetAll(), ALeft);
341 	// add components
342 	AddElement(pDesc);
343 	AddElement(pPing);
344 	if (pDisconnectBtn) AddElement(pDisconnectBtn);
345 	// add to listbox (will eventually get moved)
346 	pForDlg->AddElement(this);
347 	// first-time update
348 	Update();
349 }
350 
GetConnection() const351 C4Network2IOConnection *C4Network2ClientListBox::ConnectionListItem::GetConnection() const
352 {
353 	// get connection by connection ID
354 	C4Network2Client *pNetClient = ::Network.Clients.GetClientByID(iClientID);
355 	if (!pNetClient) return nullptr;
356 	if (iConnID == 0) return pNetClient->getDataConn();
357 	if (iConnID == 1) return pNetClient->getMsgConn();
358 	return nullptr;
359 }
360 
Update()361 void C4Network2ClientListBox::ConnectionListItem::Update()
362 {
363 	C4Network2IOConnection *pConn = GetConnection();
364 	if (!pConn)
365 	{
366 		// No connection: Shouldn't happen
367 		pDesc->SetText("???");
368 		pPing->SetText("???");
369 		return;
370 	}
371 	// update connection ping
372 	int iPing = pConn->getLag();
373 	pPing->SetText(FormatString("%d ms", iPing).getData());
374 	// update description
375 	// get connection usage
376 	const char *szConnType;
377 	C4Network2Client *pNetClient = ::Network.Clients.GetClientByID(iClientID);
378 	if (pNetClient->getDataConn() == pNetClient->getMsgConn())
379 		szConnType = "Data/Msg";
380 	else if (iConnID)
381 		szConnType = "Msg";
382 	else
383 		szConnType = "Data";
384 	// display info
385 	pDesc->SetText(FormatString("%s: %s (%s l%d)",
386 	                            szConnType,
387 	                            ::Network.NetIO.getNetIOName(pConn->getNetClass()),
388 	                            pConn->getPeerAddr().ToString().getData(),
389 	                            pConn->getPacketLoss()).getData());
390 }
391 
OnButtonDisconnect(C4GUI::Control * pButton)392 void C4Network2ClientListBox::ConnectionListItem::OnButtonDisconnect(C4GUI::Control *pButton)
393 {
394 	// close connection
395 	C4Network2IOConnection *pConn = GetConnection();
396 	if (pConn)
397 	{
398 		pConn->Close();
399 	}
400 }
401 
OnButtonReconnect(C4GUI::Control * pButton)402 void C4Network2ClientListBox::ConnectionListItem::OnButtonReconnect(C4GUI::Control *pButton)
403 {
404 	// 2do
405 }
406 
407 
408 // --------------------------------------------------
409 // C4Network2ClientListBox
410 
C4Network2ClientListBox(C4Rect & rcBounds,bool fStartup)411 C4Network2ClientListBox::C4Network2ClientListBox(C4Rect &rcBounds, bool fStartup) : ListBox(rcBounds), fStartup(fStartup)
412 {
413 	// hook into timer callback
414 	Application.Add(this);
415 	// initial update
416 	Update();
417 }
418 
~C4Network2ClientListBox()419 C4Network2ClientListBox::~C4Network2ClientListBox()
420 {
421 	Application.Remove(this);
422 }
423 
Update()424 void C4Network2ClientListBox::Update()
425 {
426 	// sync with client list
427 	ListItem *pItem = static_cast<ListItem *>(pClientWindow->GetFirst()), *pNext;
428 	const C4Client *pClient = nullptr;
429 	while ((pClient = Game.Clients.getClient(pClient)))
430 	{
431 		// skip host in startup board
432 		if (IsStartup() && pClient->isHost()) continue;
433 		// deleted client(s) present? this will also delete unneeded client connections of previous client
434 		while (pItem && (pItem->GetClientID() < pClient->getID()))
435 		{
436 			pNext = static_cast<ListItem *>(pItem->GetNext());
437 			delete pItem; pItem = pNext;
438 		}
439 		// same present for update?
440 		// need not check for connection ID, because a client item will always be placed before the corresponding connection items
441 		if (pItem && pItem->GetClientID() == pClient->getID())
442 		{
443 			pItem->Update();
444 			pItem = static_cast<ListItem *>(pItem->GetNext());
445 		}
446 		else
447 			// not present: insert (or add if pItem=nullptr)
448 			InsertElement(new ClientListItem(this, pClient->getID()), pItem);
449 		// update connections for client
450 		// but no connections in startup board
451 		if (IsStartup()) continue;
452 		// enumerate client connections
453 		C4Network2Client *pNetClient = pClient->getNetClient();
454 		if (!pNetClient) continue; // local client does not have connections
455 		C4Network2IOConnection *pLastConn = nullptr;
456 		for (int i = 0; i<2; ++i)
457 		{
458 			C4Network2IOConnection *pConn = i ? pNetClient->getMsgConn() : pNetClient->getDataConn();
459 			if (!pConn) continue;
460 			if (pConn == pLastConn) continue; // combined connection: Display only one
461 			pLastConn = pConn;
462 			// del leading items
463 			while (pItem && ((pItem->GetClientID() < pClient->getID()) || ((pItem->GetClientID() == pClient->getID()) && (pItem->GetConnectionID() < i))))
464 			{
465 				pNext = static_cast<ListItem *>(pItem->GetNext());
466 				delete pItem; pItem = pNext;
467 			}
468 			// update connection item
469 			if (pItem && pItem->GetClientID() == pClient->getID() && pItem->GetConnectionID() == i)
470 			{
471 				pItem->Update();
472 				pItem = static_cast<ListItem *>(pItem->GetNext());
473 			}
474 			else
475 			{
476 				// new connection: create it
477 				InsertElement(new ConnectionListItem(this, pClient->getID(), i), pItem);
478 			}
479 		}
480 	}
481 	// del trailing items
482 	while (pItem)
483 	{
484 		pNext = static_cast<ListItem *>(pItem->GetNext());
485 		delete pItem; pItem = pNext;
486 	}
487 }
488 
SetClientSoundIcon(int32_t client_id)489 void C4Network2ClientListBox::SetClientSoundIcon(int32_t client_id)
490 {
491 	// sound icon on client element
492 	ClientListItem *item = GetClientListItem(client_id);
493 	if (item) item->SetSoundIcon();
494 }
495 
496 
497 // --------------------------------------------------
498 // C4Network2ClientListDlg
499 
500 // singleton
501 C4Network2ClientListDlg *C4Network2ClientListDlg::pInstance = nullptr;
502 
C4Network2ClientListDlg()503 C4Network2ClientListDlg::C4Network2ClientListDlg()
504 		: Dialog(::pGUI->GetPreferredDlgRect().Wdt*3/4, ::pGUI->GetPreferredDlgRect().Hgt*3/4, LoadResStr("IDS_NET_CAPTION"), false)
505 {
506 	// component layout
507 	CStdFont *pUseFont = &::GraphicsResource.TextFont;
508 	C4GUI::ComponentAligner caAll(GetContainedClientRect(), 0,0);
509 	C4Rect rcStatus = caAll.GetFromBottom(pUseFont->GetLineHeight());
510 	// create game options; max 1/2 of dialog height
511 	pGameOptions = new C4GameOptionsList(caAll.GetFromTop(caAll.GetInnerHeight()/2), true, C4GameOptionsList::GOLS_Runtime);
512 	pGameOptions->SetDecoration(false, nullptr, true, false);
513 	pGameOptions->SetSelectionDiabled();
514 	// but resize to actually used height
515 	int32_t iFreedHeight = pGameOptions->ContractToElementHeight();
516 	caAll.ExpandTop(iFreedHeight);
517 	AddElement(pGameOptions);
518 	// create client list box
519 	AddElement(pListBox = new C4Network2ClientListBox(caAll.GetAll(), false));
520 	// create status label
521 	AddElement(pStatusLabel = new C4GUI::Label("", rcStatus));
522 	// add timer
523 	Application.Add(this);
524 	// initial update
525 	Update();
526 }
527 
~C4Network2ClientListDlg()528 C4Network2ClientListDlg::~C4Network2ClientListDlg()
529 {
530 	if (this==pInstance) pInstance=nullptr; Application.Remove(this);
531 }
532 
Update()533 void C4Network2ClientListDlg::Update()
534 {
535 	// Compose status text
536 	StdStrBuf sStatusText;
537 	sStatusText.Format("Tick %d, Behind %d, Rate %d, PreSend %d, ACT: %d",
538 	                   (int)::Control.ControlTick, (int)::Control.Network.GetBehind(::Control.ControlTick),
539 	                   (int)::Control.ControlRate, (int)::Control.Network.getControlPreSend(),
540 	                   (int)::Control.Network.getAvgControlSendTime());
541 	// Update status label
542 	pStatusLabel->SetText(sStatusText.getData());
543 }
544 
Toggle()545 bool C4Network2ClientListDlg::Toggle()
546 {
547 	// toggle off?
548 	if (pInstance) { pInstance->Close(true); return true; }
549 	// toggle on!
550 	return ::pGUI->ShowRemoveDlg(pInstance = new C4Network2ClientListDlg());
551 }
552 
OnSound(class C4Client * singer)553 void C4Network2ClientListDlg::OnSound(class C4Client *singer)
554 {
555 	if (singer) pListBox->SetClientSoundIcon(singer->getID());
556 }
557 
558 
559 // --------------------------------------------------
560 // C4Network2StartWaitDlg
561 
C4Network2StartWaitDlg()562 C4Network2StartWaitDlg::C4Network2StartWaitDlg()
563 		: C4GUI::Dialog(DialogWidth, DialogHeight, LoadResStr("IDS_NET_CAPTION"), false)
564 {
565 	C4GUI::ComponentAligner caAll(GetContainedClientRect(), C4GUI_DefDlgIndent, C4GUI_DefDlgIndent);
566 	C4GUI::ComponentAligner caButtonArea(caAll.GetFromBottom(C4GUI_ButtonAreaHgt), 0,0);
567 	// create top label
568 	C4GUI::Label *pLbl;
569 	AddElement(pLbl = new C4GUI::Label(LoadResStr("IDS_NET_WAITFORSTART"), caAll.GetFromTop(25), ACenter));
570 	// create client list box
571 	AddElement(pClientListBox = new C4Network2ClientListBox(caAll.GetAll(), true));
572 	// place abort button
573 	C4GUI::Button *pBtnAbort = new C4GUI::CancelButton(caButtonArea.GetCentered(C4GUI_DefButtonWdt, C4GUI_ButtonHgt));
574 	AddElement(pBtnAbort);
575 }
576 
577 
578 
579 // ---------------------------------------------------
580 // C4GameOptionButtons
581 
C4GameOptionButtons(const C4Rect & rcBounds,bool fNetwork,bool fHost,bool fLobby)582 C4GameOptionButtons::C4GameOptionButtons(const C4Rect &rcBounds, bool fNetwork, bool fHost, bool fLobby)
583 		: C4GUI::Window(), fNetwork(fNetwork), fHost(fHost), fLobby(fLobby), fCountdown(false)
584 {
585 	SetBounds(rcBounds);
586 	// calculate button size from area
587 	int32_t iButtonCount = fNetwork ? fHost ? 6 : 3 : 2;
588 	int32_t iIconSize = std::min<int32_t>(C4GUI_IconExHgt, rcBounds.Hgt), iIconSpacing = rcBounds.Wdt/(rcBounds.Wdt >= 400 ? 64 : 128);
589 	if ((iIconSize+iIconSpacing*2)*iButtonCount > rcBounds.Wdt)
590 	{
591 		if (iIconSize*iButtonCount <= rcBounds.Wdt)
592 		{
593 			iIconSpacing = std::max<int32_t>(0, (rcBounds.Wdt-iIconSize*iButtonCount)/(iButtonCount*2)-1);
594 		}
595 		else
596 		{
597 			iIconSpacing = 0;
598 			iIconSize = rcBounds.Wdt / iButtonCount;
599 		}
600 	}
601 	C4GUI::ComponentAligner caButtonArea(rcBounds,0,0,true);
602 	C4GUI::ComponentAligner caButtons(caButtonArea.GetCentered((iIconSize+2*iIconSpacing)*iButtonCount, iIconSize),iIconSpacing,0);
603 	// add buttons
604 	if (fNetwork && fHost)
605 	{
606 		bool fIsInternet = !!Config.Network.MasterServerSignUp, fIsDisabled = false;
607 		// league currently implies master server signup, and can thus not be turned off
608 		if (fLobby && Game.Parameters.isLeague())
609 		{
610 			fIsInternet = true;
611 			fIsDisabled = true;
612 		}
613 		btnInternet = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(fIsInternet ? C4GUI::Ico_Ex_InternetOn : C4GUI::Ico_Ex_InternetOff, caButtons.GetFromLeft(iIconSize, iIconSize), LoadResStr("IDS_DLGTIP_STARTINTERNETGAME"), &C4GameOptionButtons::OnBtnInternet, this);
614 		btnInternet->SetEnabled(!fIsDisabled);
615 		AddElement(btnInternet);
616 	}
617 	else btnInternet = nullptr;
618 	bool fIsLeague = false;
619 	// League button
620 	if (fNetwork)
621 	{
622 		C4GUI::Icons eLeagueIcon;
623 		fIsLeague = fLobby ? Game.Parameters.isLeague() : !!Config.Network.LeagueServerSignUp;
624 		eLeagueIcon = fIsLeague ? C4GUI::Ico_Ex_LeagueOn : C4GUI::Ico_Ex_LeagueOff;
625 		btnLeague = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(eLeagueIcon, caButtons.GetFromLeft(iIconSize, iIconSize), LoadResStr("IDS_DLGTIP_STARTLEAGUEGAME"), &C4GameOptionButtons::OnBtnLeague, this);
626 		btnLeague->SetEnabled(fHost && !fLobby);
627 		AddElement(btnLeague);
628 	}
629 	else btnLeague=nullptr;
630 	if (fNetwork && fHost)
631 	{
632 		btnPassword = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(::Network.isPassworded() ? C4GUI::Ico_Ex_Locked : C4GUI::Ico_Ex_Unlocked, caButtons.GetFromLeft(iIconSize, iIconSize), LoadResStr("IDS_NET_PASSWORD_DESC"), &C4GameOptionButtons::OnBtnPassword, this);
633 		AddElement(btnPassword);
634 		btnComment = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(C4GUI::Ico_Ex_Comment, caButtons.GetFromLeft(iIconSize, iIconSize), LoadResStr("IDS_DESC_COMMENTDESCRIPTIONFORTHIS"), &C4GameOptionButtons::OnBtnComment, this);
635 		AddElement(btnComment);
636 	}
637 	else btnPassword=btnComment=nullptr;
638 	btnRecord = new C4GUI::CallbackButton<C4GameOptionButtons, C4GUI::IconButton>(Game.Record || fIsLeague ? C4GUI::Ico_Ex_RecordOn : C4GUI::Ico_Ex_RecordOff, caButtons.GetFromLeft(iIconSize, iIconSize), LoadResStr("IDS_DLGTIP_RECORD"), &C4GameOptionButtons::OnBtnRecord, this);
639 	btnRecord->SetEnabled(!fIsLeague);
640 	AddElement(btnRecord);
641 }
642 
OnBtnInternet(C4GUI::Control * btn)643 void C4GameOptionButtons::OnBtnInternet(C4GUI::Control *btn)
644 {
645 	if (!fNetwork || !fHost) return;
646 	bool fCheck = (Config.Network.MasterServerSignUp = !Config.Network.MasterServerSignUp);
647 	// in lobby mode, do actual termination from masterserver
648 	if (fLobby)
649 	{
650 		if (fCheck)
651 		{
652 			fCheck = ::Network.LeagueSignupEnable();
653 		}
654 		else
655 		{
656 			::Network.LeagueSignupDisable();
657 		}
658 	}
659 	btnInternet->SetIcon(fCheck ? C4GUI::Ico_Ex_InternetOn : C4GUI::Ico_Ex_InternetOff);
660 	// also update league button, because turning off masterserver also turns off the league
661 	if (!fCheck)
662 	{
663 		Config.Network.LeagueServerSignUp = false;
664 		if (btnLeague)
665 			btnLeague->SetIcon(C4GUI::Ico_Ex_LeagueOff);
666 	}
667 	// re-set in config for the case of failure
668 	Config.Network.MasterServerSignUp = fCheck;
669 }
670 
OnBtnLeague(C4GUI::Control * btn)671 void C4GameOptionButtons::OnBtnLeague(C4GUI::Control *btn)
672 {
673 	if (!fNetwork || !fHost) return;
674 	bool fCheck = (Config.Network.LeagueServerSignUp = !Config.Network.LeagueServerSignUp);
675 	btnLeague->SetIcon(fCheck ? C4GUI::Ico_Ex_LeagueOn : C4GUI::Ico_Ex_LeagueOff);
676 	if (!Game.Record) OnBtnRecord(btnRecord);
677 	btnRecord->SetEnabled(!fCheck);
678 	// if the league is turned on, the game must be signed up at the masterserver
679 	if (fCheck && !Config.Network.MasterServerSignUp) OnBtnInternet(btnInternet);
680 	// refresh options in scenario selection dialogue
681 	if (C4Startup::Get()) C4Startup::Get()->OnLeagueOptionChanged();
682 }
683 
OnBtnRecord(C4GUI::Control * btn)684 void C4GameOptionButtons::OnBtnRecord(C4GUI::Control *btn)
685 {
686 	Game.Record = !Game.Record;
687 	Config.General.DefRec = Game.Record;
688 	btnRecord->SetIcon(Game.Record ? C4GUI::Ico_Ex_RecordOn : C4GUI::Ico_Ex_RecordOff);
689 }
690 
OnBtnPassword(C4GUI::Control * btn)691 void C4GameOptionButtons::OnBtnPassword(C4GUI::Control *btn)
692 {
693 	if (!fNetwork || !fHost) return;
694 	// password is currently set - a single click only clears the password
695 	if (::Network.GetPassword() && *::Network.GetPassword())
696 	{
697 		StdStrBuf empty;
698 		OnPasswordSet(empty);
699 		return;
700 	}
701 	// password button pressed: Show dialog to set/change current password
702 	C4GUI::InputDialog *pDlg;
703 	GetScreen()->ShowRemoveDlg(pDlg=new C4GUI::InputDialog(LoadResStr("IDS_MSG_ENTERPASSWORD"), LoadResStr("IDS_DLG_PASSWORD"), C4GUI::Ico_Ex_LockedFrontal, new C4GUI::InputCallback<C4GameOptionButtons>(this, &C4GameOptionButtons::OnPasswordSet), false));
704 	pDlg->SetMaxText(CFG_MaxString);
705 	const char *szPassPreset = ::Network.GetPassword();
706 	if (!szPassPreset || !*szPassPreset) szPassPreset = Config.Network.LastPassword;
707 	if (*szPassPreset) pDlg->SetInputText(szPassPreset);
708 }
709 
OnPasswordSet(const StdStrBuf & rsNewPassword)710 void C4GameOptionButtons::OnPasswordSet(const StdStrBuf &rsNewPassword)
711 {
712 	// password input dialog answered with OK: Set/clear network password
713 	const char *szPass;
714 	::Network.SetPassword(szPass=rsNewPassword.getData());
715 	// update icon to reflect if a password is set
716 	UpdatePasswordBtn();
717 	// remember password for next round
718 	bool fHasPassword = (szPass && *szPass);
719 	if (fHasPassword)
720 	{
721 		SCopy(szPass, Config.Network.LastPassword, CFG_MaxString);
722 	}
723 	// acoustic feedback
724 	C4GUI::GUISound("UI::Confirmed");
725 }
726 
UpdatePasswordBtn()727 void C4GameOptionButtons::UpdatePasswordBtn()
728 {
729 	// update icon to reflect if a password is set
730 	const char *szPass = ::Network.GetPassword();
731 	bool fHasPassword = szPass && *szPass;
732 	btnPassword->SetIcon(fHasPassword ? C4GUI::Ico_Ex_Locked : C4GUI::Ico_Ex_Unlocked);
733 }
734 
OnBtnComment(C4GUI::Control * btn)735 void C4GameOptionButtons::OnBtnComment(C4GUI::Control *btn)
736 {
737 	// password button pressed: Show dialog to set/change current password
738 	C4GUI::InputDialog *pDlg;
739 	GetScreen()->ShowRemoveDlg(pDlg=new C4GUI::InputDialog(LoadResStr("IDS_CTL_ENTERCOMMENT"), LoadResStr("IDS_CTL_COMMENT"), C4GUI::Ico_Ex_Comment, new C4GUI::InputCallback<C4GameOptionButtons>(this, &C4GameOptionButtons::OnCommentSet), false));
740 	pDlg->SetMaxText(C4MaxComment);
741 	pDlg->SetInputText(Config.Network.Comment.getData());
742 }
743 
OnCommentSet(const StdStrBuf & rsNewComment)744 void C4GameOptionButtons::OnCommentSet(const StdStrBuf &rsNewComment)
745 {
746 	// check for change; no reference invalidation if not changed
747 	if (rsNewComment == Config.Network.Comment) return;
748 	// Set in configuration, update reference
749 	Config.Network.Comment.CopyValidated(rsNewComment.getData());
750 	::Network.InvalidateReference();
751 	// message feedback
752 	Log(LoadResStr("IDS_NET_COMMENTCHANGED"));
753 	// acoustic feedback
754 	C4GUI::GUISound("UI::Confirmed");
755 }
756 
SetCountdown(bool fToVal)757 void C4GameOptionButtons::SetCountdown(bool fToVal)
758 {
759 	fCountdown = fToVal;
760 }
761 
762 // ---------------------------------------------------
763 // C4Chart
764 
GetValueDecade(int iVal)765 int GetValueDecade(int iVal)
766 {
767 	// get enclosing decade
768 	int iDec = 1;
769 	while (iVal) { iVal/=10; iDec*=10; }
770 	return iDec;
771 }
772 
GetAxisStepRange(int iRange,int iMaxSteps)773 int GetAxisStepRange(int iRange, int iMaxSteps)
774 {
775 	// try in steps of 5s and 10s
776 	int iDec = GetValueDecade(iRange);
777 	if (iDec == 1) return 1;
778 	int iNextStepDivider = 2;
779 	while (iDec>=iNextStepDivider && iNextStepDivider*iRange/iDec <= iMaxSteps)
780 	{
781 		iDec/=iNextStepDivider;
782 		iNextStepDivider = 7 - iNextStepDivider;
783 	}
784 	return iDec;
785 }
786 
DrawElement(C4TargetFacet & cgo)787 void C4Chart::DrawElement(C4TargetFacet &cgo)
788 {
789 	typedef C4Graph::ValueType ValueType;
790 	typedef C4Graph::TimeType TimeType;
791 	// transparent w/o graph
792 	if (!pDisplayGraph) return;
793 	int iSeriesCount = pDisplayGraph->GetSeriesCount();
794 	if (!iSeriesCount) return;
795 	assert(iSeriesCount>0);
796 	StdStrBuf sbuf;
797 	pDisplayGraph->Update(); // update averages, etc.
798 	// calc metrics
799 	CStdFont &rFont = ::GraphicsResource.MiniFont;
800 	int       YAxisWdt       = 5,
801 	          XAxisHgt       = 15;
802 
803 	const int AxisArrowLen   = 6,
804 	          AxisMarkerLen  = 5,
805 	          AxisArrowThickness = 3,
806 	          AxisArrowIndent=  2; // margin between axis arrow and last value
807 	int32_t       YAxisMinStepHgt, XAxisMinStepWdt;
808 	// get value range
809 	int iMinTime = pDisplayGraph->GetStartTime();
810 	int iMaxTime = pDisplayGraph->GetEndTime() - 1;
811 	if (iMinTime >= iMaxTime) return;
812 	ValueType iMinVal = pDisplayGraph->GetMinValue();
813 	ValueType iMaxVal = pDisplayGraph->GetMaxValue();
814 	if (iMinVal == iMaxVal) ++iMaxVal;
815 	if (iMinVal > 0 && iMaxVal/iMinVal >= 2) iMinVal = 0; // go zero-based if this creates less than 50% unused space
816 	else if (iMaxVal < 0 && iMinVal/iMaxVal >= 2) iMaxVal = 0;
817 	int ddv;
818 	if (iMaxVal>0 && (ddv=GetValueDecade(int(iMaxVal))/50))
819 		iMaxVal = ((iMaxVal-(iMaxVal>0))/ddv+(iMaxVal>0))*ddv;
820 	if (iMinVal && (ddv=GetValueDecade(int(iMinVal))/50))
821 		iMinVal = ((iMinVal-(iMinVal<0))/ddv+(iMinVal<0))*ddv;
822 	ValueType dv=iMaxVal-iMinVal; TimeType dt=iMaxTime-iMinTime;
823 	// axis calculations
824 	sbuf.Format("-%d", (int) std::max(Abs(iMaxVal), Abs(iMinVal)));
825 	rFont.GetTextExtent(sbuf.getData(), XAxisMinStepWdt, YAxisMinStepHgt, false);
826 	YAxisWdt += XAxisMinStepWdt; XAxisHgt += YAxisMinStepHgt;
827 	XAxisMinStepWdt += 2; YAxisMinStepHgt += 2;
828 	int tw = rcBounds.Wdt - YAxisWdt;
829 	int th = rcBounds.Hgt - XAxisHgt;
830 	int tx = rcBounds.x + int(cgo.TargetX) + YAxisWdt;
831 	int ty = rcBounds.y + int(cgo.TargetY);
832 	// show a legend, if more than one graph is shown
833 	if (iSeriesCount > 1)
834 	{
835 		int iSeries = 0; const C4Graph *pSeries;
836 		int32_t iLegendWdt = 0, Q,W;
837 		while ((pSeries = pDisplayGraph->GetSeries(iSeries++)))
838 		{
839 			rFont.GetTextExtent(pSeries->GetTitle(), W, Q, true);
840 			iLegendWdt = std::max(iLegendWdt, W);
841 		}
842 		tw -= iLegendWdt+1;
843 		iSeries = 0;
844 		int iYLegendDraw = (th - iSeriesCount*Q)/2 + ty;
845 		while ((pSeries = pDisplayGraph->GetSeries(iSeries++)))
846 		{
847 			pDraw->TextOut(pSeries->GetTitle(), rFont, 1.0f, cgo.Surface, tx+tw, iYLegendDraw, pSeries->GetColorDw() | 0xff000000, ALeft, true);
848 			iYLegendDraw += Q;
849 		}
850 	}
851 	// safety: too small?
852 	if (tw < 10 || th < 10) return;
853 	// draw axis
854 	pDraw->DrawLineDw(cgo.Surface, tx, ty+th, tx+tw-1, ty+th, C4RGB(0x91, 0x91, 0x91));
855 	pDraw->DrawLineDw(cgo.Surface, tx+tw-1, ty+th, tx+tw-1-AxisArrowLen, ty+th-AxisArrowThickness, C4RGB(0x91, 0x91, 0x91));
856 	pDraw->DrawLineDw(cgo.Surface, tx+tw-1, ty+th, tx+tw-1-AxisArrowLen, ty+th+AxisArrowThickness, C4RGB(0x91, 0x91, 0x91));
857 	pDraw->DrawLineDw(cgo.Surface, tx, ty, tx, ty+th, C4RGB(0x91, 0x91, 0x91));
858 	pDraw->DrawLineDw(cgo.Surface, tx, ty, tx-AxisArrowThickness, ty+AxisArrowLen, C4RGB(0x91, 0x91, 0x91));
859 	pDraw->DrawLineDw(cgo.Surface, tx, ty, tx+AxisArrowThickness, ty+AxisArrowLen, C4RGB(0x91, 0x91, 0x91));
860 	tw -= AxisArrowLen + AxisArrowIndent;
861 	th -= AxisArrowLen + AxisArrowIndent; ty += AxisArrowLen + AxisArrowIndent;
862 	// do axis numbering
863 	int iXAxisSteps = GetAxisStepRange(dt, tw / XAxisMinStepWdt),
864 	                  iYAxisSteps = GetAxisStepRange(int(dv), th / YAxisMinStepHgt);
865 	int iX, iY, iTime, iVal;
866 	iY = 0;
867 	iTime = ((iMinTime-(iMinTime>0))/iXAxisSteps+(iMinTime>0))*iXAxisSteps;
868 	for (; iTime <= iMaxTime; iTime += iXAxisSteps)
869 	{
870 		iX = tx + tw * (iTime-iMinTime) / dt;
871 		pDraw->DrawLineDw(cgo.Surface, iX, ty+th+1, iX, ty+th+AxisMarkerLen, C4RGB(0x91, 0x91, 0x91));
872 		sbuf.Format("%d", (int) iTime);
873 		pDraw->TextOut(sbuf.getData(), rFont, 1.0f, cgo.Surface, iX, ty+th+AxisMarkerLen, 0xff7f7f7f, ACenter, false);
874 	}
875 	iVal = int( ((iMinVal-(iMinVal>0))/iYAxisSteps+(iMinVal>0))*iYAxisSteps );
876 	for (; iVal <= iMaxVal; iVal += iYAxisSteps)
877 	{
878 		iY = ty+th - int((iVal-iMinVal) / dv * th);
879 		pDraw->DrawLineDw(cgo.Surface, tx-AxisMarkerLen, iY, tx-1, iY, C4RGB(0x91, 0x91, 0x91));
880 		sbuf.Format("%d", (int) iVal);
881 		pDraw->TextOut(sbuf.getData(), rFont, 1.0f, cgo.Surface, tx-AxisMarkerLen, iY-rFont.GetLineHeight()/2, 0xff7f7f7f, ARight, false);
882 	}
883 	// draw graph series(es)
884 	int iSeries = 0;
885 	while (const C4Graph *pSeries = pDisplayGraph->GetSeries(iSeries++))
886 	{
887 		int iThisMinTime = std::max(iMinTime, pSeries->GetStartTime());
888 		int iThisMaxTime = std::min(iMaxTime, pSeries->GetEndTime());
889 		bool fAnyVal = false;
890 		for (iX = 0; iX<tw; ++iX)
891 		{
892 			iTime = iMinTime + dt*iX/tw;
893 			if (!Inside(iTime, iThisMinTime, iThisMaxTime)) continue;
894 			int iY2 = int((-pSeries->GetValue(iTime) + iMinVal) * th / dv) + ty+th;
895 			if (fAnyVal) pDraw->DrawLineDw(cgo.Surface, (float) (tx+iX-1), (float) iY, (float) (tx+iX), (float) iY2, pSeries->GetColorDw() | 0xff000000);
896 			iY = iY2;
897 			fAnyVal = true;
898 		}
899 	}
900 }
901 
C4Chart(C4Rect & rcBounds)902 C4Chart::C4Chart(C4Rect &rcBounds) : Element(), pDisplayGraph(nullptr), fOwnGraph(false)
903 {
904 	this->rcBounds = rcBounds;
905 }
906 
~C4Chart()907 C4Chart::~C4Chart()
908 {
909 	if (fOwnGraph && pDisplayGraph) delete pDisplayGraph;
910 }
911 
912 
913 // singleton
914 C4ChartDialog *C4ChartDialog::pChartDlg=nullptr;
915 
C4ChartDialog()916 C4ChartDialog::C4ChartDialog() : Dialog(DialogWidth, DialogHeight, LoadResStr("IDS_NET_STATISTICS"), false)
917 {
918 	// register singleton
919 	pChartDlg = this;
920 	// add main chart switch component
921 	C4GUI::ComponentAligner caAll(GetContainedClientRect(), 5,5);
922 	pChartTabular = new C4GUI::Tabular(caAll.GetAll(), C4GUI::Tabular::tbTop);
923 	AddElement(pChartTabular);
924 	// add some graphs as subcomponents
925 	AddChart(StdStrBuf("oc"));
926 	AddChart(StdStrBuf("FPS"));
927 	AddChart(StdStrBuf("NetIO"));
928 	if (::Network.isEnabled())
929 		AddChart(StdStrBuf("Pings"));
930 	AddChart(StdStrBuf("Control"));
931 	AddChart(StdStrBuf("APM"));
932 }
933 
AddChart(const StdStrBuf & rszName)934 void C4ChartDialog::AddChart(const StdStrBuf &rszName)
935 {
936 	// get graph by name
937 	if (!Game.pNetworkStatistics || !pChartTabular) return;
938 	bool fOwnGraph = false;
939 	C4Graph *pGraph = Game.pNetworkStatistics->GetGraphByName(rszName, fOwnGraph);
940 	if (!pGraph) return;
941 	// add sheet for name
942 	C4GUI::Tabular::Sheet *pSheet = pChartTabular->AddSheet(rszName.getData());
943 	if (!pSheet) { if (fOwnGraph) delete pGraph; return; }
944 	// add chart to sheet
945 	C4Chart *pNewChart = new C4Chart(pSheet->GetClientRect());
946 	pNewChart->SetGraph(pGraph, fOwnGraph);
947 	pSheet->AddElement(pNewChart);
948 }
949 
Toggle()950 void C4ChartDialog::Toggle()
951 {
952 	// close if open
953 	if (pChartDlg) { pChartDlg->Close(false); return; }
954 	// otherwise, open
955 	C4ChartDialog *pDlg = new C4ChartDialog();
956 	if (!::pGUI->ShowRemoveDlg(pDlg)) if (pChartDlg) delete pChartDlg;
957 }
958