1/*
2 ** st_console.mm
3 **
4 **---------------------------------------------------------------------------
5 ** Copyright 2015 Alexey Lysiuk
6 ** All rights reserved.
7 **
8 ** Redistribution and use in source and binary forms, with or without
9 ** modification, are permitted provided that the following conditions
10 ** are met:
11 **
12 ** 1. Redistributions of source code must retain the above copyright
13 **    notice, this list of conditions and the following disclaimer.
14 ** 2. Redistributions in binary form must reproduce the above copyright
15 **    notice, this list of conditions and the following disclaimer in the
16 **    documentation and/or other materials provided with the distribution.
17 ** 3. The name of the author may not be used to endorse or promote products
18 **    derived from this software without specific prior written permission.
19 **
20 ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
21 ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
22 ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
23 ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
24 ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
25 ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
29 ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 **---------------------------------------------------------------------------
31 **
32 */
33
34#include "i_common.h"
35
36#include "d_main.h"
37#include "i_system.h"
38#include "st_console.h"
39#include "v_text.h"
40#include "version.h"
41
42
43static NSColor* RGB(const BYTE red, const BYTE green, const BYTE blue)
44{
45	return [NSColor colorWithCalibratedRed:red   / 255.0f
46									 green:green / 255.0f
47									  blue:blue  / 255.0f
48									 alpha:1.0f];
49}
50
51static NSColor* RGB(const PalEntry& color)
52{
53	return RGB(color.r, color.g, color.b);
54}
55
56static NSColor* RGB(const DWORD color)
57{
58	return RGB(PalEntry(color));
59}
60
61
62static const CGFloat PROGRESS_BAR_HEIGHT = 18.0f;
63static const CGFloat NET_VIEW_HEIGHT     = 88.0f;
64
65
66FConsoleWindow::FConsoleWindow()
67: m_window([NSWindow alloc])
68, m_textView([NSTextView alloc])
69, m_scrollView([NSScrollView alloc])
70, m_progressBar(nil)
71, m_netView(nil)
72, m_netMessageText(nil)
73, m_netCountText(nil)
74, m_netProgressBar(nil)
75, m_netAbortButton(nil)
76, m_characterCount(0)
77, m_netCurPos(0)
78, m_netMaxPos(0)
79{
80	const CGFloat initialWidth  = 512.0f;
81	const CGFloat initialHeight = 384.0f;
82	const NSRect initialRect = NSMakeRect(0.0f, 0.0f, initialWidth, initialHeight);
83
84	[m_textView initWithFrame:initialRect];
85	[m_textView setEditable:NO];
86	[m_textView setBackgroundColor:RGB(70, 70, 70)];
87	[m_textView setMinSize:NSMakeSize(0.0f, initialHeight)];
88	[m_textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
89	[m_textView setVerticallyResizable:YES];
90	[m_textView setHorizontallyResizable:NO];
91	[m_textView setAutoresizingMask:NSViewWidthSizable];
92
93	NSTextContainer* const textContainer = [m_textView textContainer];
94	[textContainer setContainerSize:NSMakeSize(initialWidth, FLT_MAX)];
95	[textContainer setWidthTracksTextView:YES];
96
97	[m_scrollView initWithFrame:NSMakeRect(0.0f, 0.0f, initialWidth, initialHeight)];
98	[m_scrollView setBorderType:NSNoBorder];
99	[m_scrollView setHasVerticalScroller:YES];
100	[m_scrollView setHasHorizontalScroller:NO];
101	[m_scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
102	[m_scrollView setDocumentView:m_textView];
103
104	NSString* const title = [NSString stringWithFormat:@"%s %s - Console", GAMESIG, GetVersionString()];
105
106	[m_window initWithContentRect:initialRect
107						styleMask:NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask
108						  backing:NSBackingStoreBuffered
109							defer:NO];
110	[m_window setMinSize:[m_window frame].size];
111	[m_window setShowsResizeIndicator:NO];
112	[m_window setTitle:title];
113	[m_window center];
114	[m_window exitAppOnClose];
115
116	[[m_window contentView] addSubview:m_scrollView];
117
118	[m_window makeKeyAndOrderFront:nil];
119}
120
121
122static FConsoleWindow* s_instance;
123
124
125void FConsoleWindow::CreateInstance()
126{
127	assert(NULL == s_instance);
128	s_instance = new FConsoleWindow;
129}
130
131void FConsoleWindow::DeleteInstance()
132{
133	assert(NULL != s_instance);
134	delete s_instance;
135	s_instance = NULL;
136}
137
138FConsoleWindow& FConsoleWindow::GetInstance()
139{
140	assert(NULL != s_instance);
141	return *s_instance;
142}
143
144
145void FConsoleWindow::Show(const bool visible)
146{
147	if (visible)
148	{
149		[m_window orderFront:nil];
150	}
151	else
152	{
153		[m_window orderOut:nil];
154	}
155}
156
157void FConsoleWindow::ShowFatalError(const char* const message)
158{
159	SetProgressBar(false);
160	NetDone();
161
162	const CGFloat textViewWidth = [m_scrollView frame].size.width;
163
164	ExpandTextView(-32.0f);
165
166	NSButton* quitButton = [[NSButton alloc] initWithFrame:NSMakeRect(textViewWidth - 76.0f, 0.0f, 72.0f, 30.0f)];
167	[quitButton setAutoresizingMask:NSViewMinXMargin];
168	[quitButton setBezelStyle:NSRoundedBezelStyle];
169	[quitButton setTitle:@"Quit"];
170	[quitButton setKeyEquivalent:@"\r"];
171	[quitButton setTarget:NSApp];
172	[quitButton setAction:@selector(terminate:)];
173
174	NSView* quitPanel = [[NSView alloc] initWithFrame:NSMakeRect(0.0f, 0.0f, textViewWidth, 32.0f)];
175	[quitPanel setAutoresizingMask:NSViewWidthSizable];
176	[quitPanel addSubview:quitButton];
177
178	[[m_window contentView] addSubview:quitPanel];
179	[m_window orderFront:nil];
180
181	AddText(PalEntry(255,   0,   0), "\nExecution could not continue.\n");
182	AddText(PalEntry(255, 255, 170), message);
183	AddText("\n");
184
185	[NSApp runModalForWindow:m_window];
186}
187
188
189void FConsoleWindow::AddText(const char* message)
190{
191	PalEntry color(223, 223, 223);
192
193	char buffer[1024] = {};
194	size_t pos = 0;
195	bool reset = false;
196
197	while (*message != '\0')
198	{
199		if ((TEXTCOLOR_ESCAPE == *message && 0 != pos)
200			|| (pos == sizeof buffer - 1)
201			|| reset)
202		{
203			buffer[pos] = '\0';
204			pos = 0;
205			reset = false;
206
207			AddText(color, buffer);
208		}
209
210#define CHECK_BUFFER_SPACE \
211	if (pos >= sizeof buffer - 3) { reset = true; continue; }
212
213		if (TEXTCOLOR_ESCAPE == *message)
214		{
215			const BYTE* colorID = reinterpret_cast<const BYTE*>(message) + 1;
216			if ('\0' == colorID)
217			{
218				break;
219			}
220
221			const EColorRange range = V_ParseFontColor(colorID, CR_UNTRANSLATED, CR_YELLOW);
222
223			if (range != CR_UNDEFINED)
224			{
225				color = V_LogColorFromColorRange(range);
226			}
227
228			message += 2;
229		}
230		else if (0x1d == *message) // Opening bar character
231		{
232			CHECK_BUFFER_SPACE;
233
234			// Insert BOX DRAWINGS LIGHT LEFT AND HEAVY RIGHT
235			buffer[pos++] = '\xe2';
236			buffer[pos++] = '\x95';
237			buffer[pos++] = '\xbc';
238			++message;
239		}
240		else if (0x1e == *message) // Middle bar character
241		{
242			CHECK_BUFFER_SPACE;
243
244			// Insert BOX DRAWINGS HEAVY HORIZONTAL
245			buffer[pos++] = '\xe2';
246			buffer[pos++] = '\x94';
247			buffer[pos++] = '\x81';
248			++message;
249		}
250		else if (0x1f == *message) // Closing bar character
251		{
252			CHECK_BUFFER_SPACE;
253
254			// Insert BOX DRAWINGS HEAVY LEFT AND LIGHT RIGHT
255			buffer[pos++] = '\xe2';
256			buffer[pos++] = '\x95';
257			buffer[pos++] = '\xbe';
258			++message;
259		}
260		else
261		{
262			buffer[pos++] = *message++;
263		}
264
265#undef CHECK_BUFFER_SPACE
266	}
267
268	if (0 != pos)
269	{
270		buffer[pos] = '\0';
271
272		AddText(color, buffer);
273	}
274
275	if ([m_window isVisible])
276	{
277		[m_textView scrollRangeToVisible:NSMakeRange(m_characterCount, 0)];
278
279		[[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode];
280	}
281}
282
283void FConsoleWindow::AddText(const PalEntry& color, const char* const message)
284{
285	NSString* const text = [NSString stringWithUTF8String:message];
286
287	NSDictionary* const attributes = [NSDictionary dictionaryWithObjectsAndKeys:
288									  [NSFont systemFontOfSize:14.0f], NSFontAttributeName,
289									  RGB(color), NSForegroundColorAttributeName,
290									  nil];
291
292	NSAttributedString* const formattedText =
293	[[NSAttributedString alloc] initWithString:text
294									attributes:attributes];
295	[[m_textView textStorage] appendAttributedString:formattedText];
296
297	m_characterCount += [text length];
298}
299
300
301void FConsoleWindow::SetTitleText()
302{
303	static const CGFloat TITLE_TEXT_HEIGHT = 32.0f;
304
305	NSRect textViewFrame = [m_scrollView frame];
306	textViewFrame.size.height -= TITLE_TEXT_HEIGHT;
307	[m_scrollView setFrame:textViewFrame];
308
309	const NSRect titleTextRect = NSMakeRect(
310		0.0f,
311		textViewFrame.origin.y + textViewFrame.size.height,
312		textViewFrame.size.width,
313		TITLE_TEXT_HEIGHT);
314
315	NSTextField* titleText = [[NSTextField alloc] initWithFrame:titleTextRect];
316	[titleText setStringValue:[NSString stringWithUTF8String:DoomStartupInfo.Name]];
317	[titleText setAlignment:NSCenterTextAlignment];
318	[titleText setTextColor:RGB(DoomStartupInfo.FgColor)];
319	[titleText setBackgroundColor:RGB(DoomStartupInfo.BkColor)];
320	[titleText setFont:[NSFont fontWithName:@"Trebuchet MS Bold" size:18.0f]];
321	[titleText setAutoresizingMask:NSViewWidthSizable | NSViewMinYMargin];
322	[titleText setSelectable:NO];
323	[titleText setBordered:NO];
324
325	[[m_window contentView] addSubview:titleText];
326}
327
328void FConsoleWindow::SetProgressBar(const bool visible)
329{
330	if (  (!visible && nil == m_progressBar)
331		|| (visible && nil != m_progressBar))
332	{
333		return;
334	}
335
336	if (visible)
337	{
338		ExpandTextView(-PROGRESS_BAR_HEIGHT);
339
340		m_progressBar = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(2.0f, 0.0f, 508.0f, 16.0f)];
341		[m_progressBar setIndeterminate:NO];
342		[m_progressBar setAutoresizingMask:NSViewWidthSizable];
343
344		[[m_window contentView] addSubview:m_progressBar];
345	}
346	else
347	{
348		ExpandTextView(PROGRESS_BAR_HEIGHT);
349
350		[m_progressBar removeFromSuperview];
351		[m_progressBar release];
352		m_progressBar = nil;
353	}
354}
355
356
357void FConsoleWindow::ExpandTextView(const float height)
358{
359	NSRect textFrame = [m_scrollView frame];
360	textFrame.origin.y    -= height;
361	textFrame.size.height += height;
362	[m_scrollView setFrame:textFrame];
363}
364
365
366void FConsoleWindow::Progress(const int current, const int maximum)
367{
368	if (nil == m_progressBar)
369	{
370		return;
371	}
372
373	static unsigned int previousTime = I_MSTime();
374	unsigned int currentTime = I_MSTime();
375
376	if (currentTime - previousTime > 33) // approx. 30 FPS
377	{
378		previousTime = currentTime;
379
380		[m_progressBar setMaxValue:maximum];
381		[m_progressBar setDoubleValue:current];
382
383		[[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode];
384	}
385}
386
387
388void FConsoleWindow::NetInit(const char* const message, const int playerCount)
389{
390	if (nil == m_netView)
391	{
392		SetProgressBar(false);
393		ExpandTextView(-NET_VIEW_HEIGHT);
394
395		// Message like 'Waiting for players' or 'Contacting host'
396		m_netMessageText = [[NSTextField alloc] initWithFrame:NSMakeRect(12.0f, 64.0f, 400.0f, 16.0f)];
397		[m_netMessageText setAutoresizingMask:NSViewWidthSizable];
398		[m_netMessageText setDrawsBackground:NO];
399		[m_netMessageText setSelectable:NO];
400		[m_netMessageText setBordered:NO];
401
402		// Text with connected/total players count
403		m_netCountText = [[NSTextField alloc] initWithFrame:NSMakeRect(428.0f, 64.0f, 72.0f, 16.0f)];
404		[m_netCountText setAutoresizingMask:NSViewMinXMargin];
405		[m_netCountText setAlignment:NSRightTextAlignment];
406		[m_netCountText setDrawsBackground:NO];
407		[m_netCountText setSelectable:NO];
408		[m_netCountText setBordered:NO];
409
410		// Connection progress
411		m_netProgressBar = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(12.0f, 40.0f, 488.0f, 16.0f)];
412		[m_netProgressBar setAutoresizingMask:NSViewWidthSizable];
413		[m_netProgressBar setMaxValue:playerCount];
414
415		if (0 == playerCount)
416		{
417			// Joining game
418			[m_netProgressBar setIndeterminate:YES];
419			[m_netProgressBar startAnimation:nil];
420		}
421		else
422		{
423			// Hosting game
424			[m_netProgressBar setIndeterminate:NO];
425		}
426
427		// Cancel network game button
428		m_netAbortButton = [[NSButton alloc] initWithFrame:NSMakeRect(432.0f, 8.0f, 72.0f, 28.0f)];
429		[m_netAbortButton setAutoresizingMask:NSViewMinXMargin];
430		[m_netAbortButton setBezelStyle:NSRoundedBezelStyle];
431		[m_netAbortButton setTitle:@"Cancel"];
432		[m_netAbortButton setKeyEquivalent:@"\r"];
433		[m_netAbortButton setTarget:NSApp];
434		[m_netAbortButton setAction:@selector(terminate:)];
435
436		// Panel for controls above
437		m_netView = [[NSView alloc] initWithFrame:NSMakeRect(0.0f, 0.0f, 512.0f, NET_VIEW_HEIGHT)];
438		[m_netView setAutoresizingMask:NSViewWidthSizable];
439		[m_netView addSubview:m_netMessageText];
440		[m_netView addSubview:m_netCountText];
441		[m_netView addSubview:m_netProgressBar];
442		[m_netView addSubview:m_netAbortButton];
443
444		NSRect windowRect = [m_window frame];
445		windowRect.origin.y    -= NET_VIEW_HEIGHT;
446		windowRect.size.height += NET_VIEW_HEIGHT;
447
448		[m_window setFrame:windowRect display:YES];
449		[[m_window contentView] addSubview:m_netView];
450	}
451
452	[m_netMessageText setStringValue:[NSString stringWithUTF8String:message]];
453
454	m_netCurPos = 0;
455	m_netMaxPos = playerCount;
456
457	NetProgress(1); // You always know about yourself
458}
459
460void FConsoleWindow::NetProgress(const int count)
461{
462	if (0 == count)
463	{
464		++m_netCurPos;
465	}
466	else
467	{
468		m_netCurPos = count;
469	}
470
471	if (nil == m_netView)
472	{
473		return;
474	}
475
476	if (m_netMaxPos > 1)
477	{
478		[m_netCountText setStringValue:[NSString stringWithFormat:@"%d / %d", m_netCurPos, m_netMaxPos]];
479		[m_netProgressBar setDoubleValue:MIN(m_netCurPos, m_netMaxPos)];
480	}
481}
482
483void FConsoleWindow::NetDone()
484{
485	if (nil != m_netView)
486	{
487		ExpandTextView(NET_VIEW_HEIGHT);
488
489		[m_netView removeFromSuperview];
490		[m_netView release];
491		m_netView = nil;
492
493		// Released by m_netView
494		m_netMessageText = nil;
495		m_netCountText = nil;
496		m_netProgressBar = nil;
497		m_netAbortButton = nil;
498	}
499}
500