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