//
// XTMainTextHandler.m
// TadsTerp
//
// Created by Rune Berg on 28/03/14.
// Copyright (c) 2014 Rune Berg. All rights reserved.
//
#import "XTBaseTextHandler_private.h"
#import "XTMainTextHandler.h"
#import "XTMainTextView.h"
#import "XTScrollView.h"
#import "XTNotifications.h"
#import "XTPrefs.h"
#import "XTOutputFormatter.h"
#import "XTFormattedOutputElement.h"
#import "XTOutputTextParserPlain.h"
#import "XTOutputTextParserHtml.h"
#import "XTHtmlTag.h"
#import "XTHtmlTagBr.h"
#import "XTHtmlTagAboutBox.h"
#import "XTHtmlTagTitle.h"
#import "XTHtmlTagBanner.h"
#import "XTHtmlTagWhitespace.h"
#import "XTHtmlTagT2TradStatusLine.h"
#import "XTLogger.h"
#import "XTStringUtils.h"
#import "XTBannerTextHandler.h"
#import "XTHtmlTagQuotedSpace.h"
#import "XTHtmlTagSpecialSpace.h"
#import "XTHtmlTagNonbreakingSpace.h"
#import "XTAllocDeallocCounter.h"
#import "XTViewLayoutUtils.h"
#import "XTTimer.h"
#import "XTMutableAttributedStringHelper.h"
#import "XTTimedCommandState.h"
@interface XTMainTextHandler ()
@property XTTimedCommandState *timedCommandState;
@property NSTimeInterval totalTimeInAppendAttributedStringToTextStorage;
@property BOOL afterNewlineAfterCommand;
@end
@implementation XTMainTextHandler
const NSUInteger initialCommandPromptPosition = NSUIntegerMax;
static XTLogger* logger;
@synthesize statusLineMode = _statusLineMode;
+ (void)initialize
{
logger = [XTLogger loggerForClass:[XTMainTextHandler class]];
}
OVERRIDE_ALLOC_FOR_COUNTER
OVERRIDE_DEALLOC_FOR_COUNTER
- (id)init
{
//XT_DEF_SELNAME;
self = [super init];
if (self) {
_commandHistory = [XTCommandHistory new];
_activeTagBannerHandle = nil;
// no text entry before first input prompt:
_commandPromptPosition = initialCommandPromptPosition;
_statusLineMode = STATUS_LINE_MODE_MAIN;
_gameTitle = [NSMutableString stringWithString:@""];
_timedCommandState = [XTTimedCommandState new];
_totalTimeInAppendAttributedStringToTextStorage = 0.0;
_afterNewlineAfterCommand = NO;
[self setupReceptionOfAppLevelNotifications];
}
return self;
}
- (void)setStatusLineMode:(NSUInteger)statusLineMode
{
//XT_DEF_SELNAME;
//XT_WARN_1(@"%lu", statusLineMode);
XTHtmlTagT2TradStatusLine *tag = [XTHtmlTagT2TradStatusLine withMode:statusLineMode];
//TODO !!! ??? flush like in oshtml_display_html_tags(const textchar_t *txt)
if (statusLineMode == STATUS_LINE_MODE_STATUS) {
int brkpt = 1;
}
[[self getOutputTextParser] appendTagToCurrentContainer:tag];
}
- (void)setStatusLineModeNow:(NSUInteger)statusLineMode
{
//XT_DEF_SELNAME;
//XT_WARN_1(@"%lu", statusLineMode);
_statusLineMode = statusLineMode;
}
- (NSUInteger)statusLineMode {
return _statusLineMode;
}
- (void)removeHandler
{
XT_DEF_SELNAME;
XT_TRACE_0(@"");
[self teardown];
self.gameWindowController = nil;
self.gameTitle = nil;
self.commandHistory = nil;
self.activeTagBannerHandle = nil;
}
+ (instancetype)handler
{
XTMainTextHandler *handler = [[XTMainTextHandler alloc] init];
return handler;
}
- (void)traceWithIndentLevel:(NSUInteger)indentLevel
{
XT_TRACE_ENTRY;
for (XTBannerTextHandler *child in self.childHandlers) {
[child traceWithIndentLevel:indentLevel + 1];
}
}
- (void)setIsForT3:(BOOL)isForT3
{
self.outputFormatter.isForT3 = isForT3;
self.colorationHelper.isForT3 = isForT3;
}
// called when game file loads and starts
- (void)resetToDefaults
{
//XT_DEF_SELNAME;
self.htmlMode = NO;
self.hiliteMode = NO;
[self setNonstopMode:NO];
self.statusLineMode = STATUS_LINE_MODE_MAIN;
self.timedCommandState = [XTTimedCommandState new];
self.afterNewlineAfterCommand = NO;
//XT_WARN_0(@"afterNewlineAfterCommand set to NO");
[self clearText];
[self.outputTextParserPlain resetForNextCommand];
[self.outputTextParserHtml resetForNextCommand];
self.activeTagBannerHandle = nil;
self.gameTitle = [NSMutableString stringWithString:@""];
}
- (void)setNonstopMode:(BOOL)nonstopMode
{
self.nonstopModeState = nonstopMode;
for (XTBannerTextHandler *child in self.childHandlers) {
child.nonstopModeState = nonstopMode;
}
}
- (void)resetForNextCommand
{
XT_DEF_SELNAME;
if ([self.timedCommandState isTimedOut]) {
XT_TRACE_0(@"aborting because timedCommandState.state == timedOut");
return;
}
//XT_WARN_0(@"executing");
[super resetForNextCommand];
[self.textStorageBatcher reset];
[self ensureInputFontIsInEffect];
self.afterNewlineAfterCommand = NO;
//XT_WARN_0(@"afterNewlineAfterCommand set to NO");
}
- (void)resetForGameHasEndedMsg
{
//XT_DEF_SELNAME;
self.timedCommandState = [XTTimedCommandState new];
[[self getOutputTextParser] flush];
[self.outputTextParserHtml resetForGameHasEndedMsg];
[self.outputTextParserPlain resetForNextCommand];
[self.outputFormatter resetFlags];
[self mainThread_noteStartOfPagination];
[self clearPaginationState];
[self.textStorageBatcher reset];
self.afterNewlineAfterCommand = NO;
//XT_WARN_0(@"afterNewlineAfterCommand set to NO");
}
- (void)exitingTagBanner
{
self.activeTagBannerHandle = NULL;
[self.outputFormatter resetTagBannerDepth];
}
- (void)createTextViewForMainOutputArea
{
XT_TRACE_ENTRY;
NSScrollView *newTextScrollView = [XTUIUtils createScrollViewWithTextViewForMainOutputArea:self.gameWindowController];
self.scrollView = newTextScrollView;
self.textView = self.scrollView.documentView;
self.outputFormatter.textView = self.textView;
self.textView.outputFormatter = self.outputFormatter;
self.textView.backgroundColor = [self.colorationHelper getOutputBackgroundColorForTextView];
self.colorationHelper = [XTColorationHelper forTextView:self.textView
isForT3:self.outputFormatter.isForT3
isForBanner:NO
isForGridBanner:NO];
self.outputFormatter.colorationHelper = self.colorationHelper;
}
- (void)noteStartOfLayoutOfViews
{
[super noteStartOfLayoutOfViews];
for (XTBannerTextHandler *child in self.childHandlers) {
[child noteStartOfLayoutOfViews];
}
}
- (void)noteEndOfLayoutOfViews
{
[super noteEndOfLayoutOfViews];
for (XTBannerTextHandler *child in self.childHandlers) {
[child noteEndOfLayoutOfViews];
}
}
- (void)mainThread_getCommand:(NSMutableArray *)returnValue
{
NSString *command = [self getCommandString];
[self.commandHistory appendCommand:command];
returnValue[0] = command;
}
- (void)mainThread_cancelTimedOutCommand:(NSArray *)args
{
BOOL reset = ((NSNumber *)args[0]).boolValue;
[self endCommandInProgressAtTimeout];
[self.timedCommandState setCancelled];
if (reset) {
self.timedCommandState.command = nil;
}
/*TODO !!! tm
if (reset) {
self.timedCommandState = [XTTimedCommandState new];
} else {
self.timedCommandState.timedOut = NO;
}*/
}
- (NSString *)getCommandString
{
NSRange range = [self getCommandTextRange];
NSString *res = [self.textView stringInRange:range];
return res;
}
- (void)clearText
{
//XT_DEF_SELNAME;
//XT_WARN_ENTRY;
// also called when game clears screen
// might be a
or something there that needs processing
[self processTagTree];
[self.textStorageBatcher reset];
[[self getOutputTextParser] flush];
[self.outputTextParserPlain resetForNextCommand];
[self.outputTextParserHtml resetForNextCommand];
[self processTagTree];
[self.outputFormatter resetFlags];
[self clearTextStorage];
self.afterNewlineAfterCommand = NO;
//XT_WARN_0(@"afterNewlineAfterCommand set to NO");
//TODO !!! rm? shouldn't be needed anymore
// Insert some temporary, invisible text to get font height set and pagination calculations correct from the start:
NSArray *mutAttrStringArray = [self.outputFormatter formatOutputText:ZERO_WIDTH_SPACE];
for (NSAttributedString *attrString in mutAttrStringArray) {
[self appendAttributedStringToTextStorage:attrString];
}
[self.outputFormatter resetFlags]; // get rid of state due to the zwsp
[self.colorationHelper resetBodyColors];
[self.textView scrollPageUp:self]; // needed to ensure new text isn't initially "scrolled past"
[self moveCursorToEndOfOutputPosition];
self.commandPromptPosition = initialCommandPromptPosition;
[self clearPaginationState];
[self mainThread_noteStartOfPagination];
// Remove the invisible text we added earlier, so that we haven't "used up"
// the text alignment of the first paragraph:
[self.textView removeLastChar];
//XT_TRACE_0(@"done");
}
- (void)flushOutput
{
XT_TRACE_ENTRY;
[[self getOutputTextParser] flush];
[self processTagTree];
[self flushFormattingQueue];
}
- (void)hardFlushOutput
{
XT_TRACE_ENTRY;
[[self getOutputTextParser] hardFlush];
[self processTagTree];
[self flushFormattingQueue];
}
- (void)appendInput:(NSString *)string
{
// Note: this is called for paste event
if (! [self canAppendNonTypedInput]) {
return;
}
NSAttributedString *attrString = [self.outputFormatter formatInputText:string];
[self appendAttributedStringToTextStorage:attrString];
[self scrollToBottom];
}
//TODO mv down
// Allow appending pasted text, text from clicked command link, etc. ?
- (BOOL)canAppendNonTypedInput
{
BOOL res = YES;
if (! self.gameWindowController.gameIsRunning) {
res = NO;
}
if ([self.gameWindowController isWaitingForKeyPressed]) {
res = NO;
}
return res;
}
- (void)handleCommandLinkClicked:(NSString *)linkText atIndex:(NSUInteger)charIndex
{
if (! [self canAppendNonTypedInput]) {
return;
}
NSRange proposedRange = NSMakeRange(charIndex, 1);
NSRange actualRange;
NSAttributedString *as = [self.textView attributedSubstringForProposedRange:proposedRange
actualRange:&actualRange];
id appendAttr = [as attribute:XT_OUTPUT_FORMATTER_ATTR_CMDLINK_APPEND atIndex:0 effectiveRange:nil];
BOOL append = (appendAttr != nil);
id noenterAttr = [as attribute:XT_OUTPUT_FORMATTER_ATTR_CMDLINK_NOENTER atIndex:0 effectiveRange:nil];
BOOL noenter = (noenterAttr != nil);
if (! append) {
[self replaceCommandText:linkText];
} else {
NSAttributedString *attrLinkString = [self.outputFormatter formatInputText:linkText];
[self appendAttributedStringToTextStorage:attrLinkString];
}
[self moveCursorToEndOfOutputPosition];
if (! noenter) {
[self.textView.delegate textView:self.textView doCommandBySelector:@selector(insertNewline:)];
}
}
- (void)ensureInputFontIsInEffect
{
//XTPrefs *prefs = [XTPrefs prefs];
//if (prefs.inputFontUsedEvenIfNotRequestedByGame.boolValue) {
[self appendInput:ZERO_WIDTH_SPACE];
[self noteEndOfOutput];
//}
}
- (BOOL)appendOutput:(NSString *)string
{
XT_DEF_SELNAME;
//XT_TRACE_0(@"-------------------------------------------------------------");
XT_TRACE_1(@"\"%@\"", string);
if ([self.timedCommandState isTimedOut]) {
XT_TRACE_0(@"return because timedCommandState.state == timedOut");
return NO;
}
if (! self.htmlMode) {
if ([string isEqualToString:@"\n"]) {
if (! self.afterNewlineAfterCommand) {
//XT_WARN_2(@"\\n - afterNewlineAfterCommand=%lu parsing \"%@\"", self.afterNewlineAfterCommand, string);
[[self getOutputTextParser] parse:string];
} else {
//XT_WARN_2(@"\\n - afterNewlineAfterCommand=%lu SKIPPING \"%@\"", self.afterNewlineAfterCommand, string);
}
} else {
[[self getOutputTextParser] parse:string];
}
self.afterNewlineAfterCommand = NO;
//XT_WARN_0(@"afterNewlineAfterCommand set to NO");
} else {
[[self getOutputTextParser] parse:string];
}
//TODO !!! rework
//BOOL excessiveAmountBuffered = (self.formattingQueue.count >= 1000);
//return excessiveAmountBuffered;
return NO;
}
- (void)appendOutputNewlineAfterCommand:(NSString *)string
{
XT_DEF_SELNAME;
if (! self.htmlMode) {
if ([string isEqualToString:@"\n"]) {
//XT_WARN_2(@"\\n - afterNewlineAfterCommand=%lu parsing \"%@\"", self.afterNewlineAfterCommand, string);
if (self.afterNewlineAfterCommand) {
XT_DEF_SELNAME;
XT_ERROR_0(@"afterNewlineAfterCommand should not be true here");
int brkpt = 1;
}
//TODO !!! exp rm:
self.afterNewlineAfterCommand = YES;
//XT_WARN_0(@"afterNewlineAfterCommand set to YES");
} else {
int brkpt = 1;
}
} else {
int brkpt = 1;
}
[[self getOutputTextParser] parse:string];
}
// the index where new input text is appended
- (NSInteger)insertionPoint
{
NSRange r = [self.textView selectedRange];
return r.location;
}
- (NSInteger)minInsertionPoint
{
NSInteger res = self.commandPromptPosition + 1;
return res;
}
- (void)moveCursorToStartOfCommand
{
NSUInteger index = self.minInsertionPoint;
[self.textView setSelectedRange:NSMakeRange(index, 0)];
}
- (void)moveCursorToEndOfOutputPosition
{
XT_DEF_SELNAME;
NSUInteger backOffset = self.timedCommandState.cursorOffsetFromEndOfCommand.location;
if (backOffset >= 1) {
NSUInteger textLength = [self.textView endOfOutputPosition];
NSUInteger minInsertionPoint = [self minInsertionPoint];
NSInteger newInsertionPoint = textLength - backOffset;
if (newInsertionPoint < minInsertionPoint) {
newInsertionPoint = minInsertionPoint;
}
NSUInteger selectedTextLength = self.timedCommandState.cursorOffsetFromEndOfCommand.length;
if (selectedTextLength >= 1) {
NSRange range = NSMakeRange(newInsertionPoint, selectedTextLength);
[self.textView setSelectedRange:range affinity: self.timedCommandState.selectedTextAffinity stillSelecting:YES];
//XT_WARN_3(@"selectedTextLength=%lu -> setSelectedRange loc=%lu len=%lu", selectedTextLength, range.location, range.length);
} else {
[self.textView setInsertionPoint:newInsertionPoint];
//XT_WARN_2(@"selectedTextLength=%lu -> setInsertionPoint=%lu", selectedTextLength, newInsertionPoint);
}
} else {
[super moveCursorToEndOfOutputPosition];
}
}
- (BOOL)cursorIsInCommand
{
BOOL res = ([self insertionPoint] >= [self minInsertionPoint]);
return res;
}
- (BOOL)cursorIsAtMinInsertionPosition
{
BOOL res = ([self insertionPoint] == [self minInsertionPoint]);
return res;
}
- (BOOL)cursorIsInCommandButNotAtMinInsertionPosition
{
//XT_DEF_SELNAME;
NSInteger insPt = [self insertionPoint];
NSInteger minInsPt = [self minInsertionPoint];
BOOL res = (insPt > minInsPt);
//XT_WARN_3(@"-> %d (insPt=%d minInsPt=%d)", res, insPt, minInsPt);
return res;
}
- (BOOL)allowTextInsertion:(NSRange)affectedCharRange
{
NSInteger minInsPt = [self minInsertionPoint];
BOOL res = (affectedCharRange.location >= minInsPt);
return res;
}
- (void)goToPreviousCommand
{
NSString *previousCommand = [self.commandHistory getPreviousCommand];
if (previousCommand != nil) {
[self replaceCommandText:previousCommand];
}
}
- (void)goToNextCommand
{
NSString *newCommandText = [self.commandHistory getNextCommand];
if (newCommandText == nil) {
if ([self.commandHistory hasBeenAccessed]) {
// we're back out of the historic commands
newCommandText = @"";
//TODO better: replace with command that was *being typed*
// - requires capturing that conmand on every keystroke
[self.commandHistory resetHasBeenAccessed];
}
}
if (newCommandText != nil) {
[self replaceCommandText:newCommandText];
}
}
- (void)replaceCommandText:(NSString *)newCommandText
{
//XT_WARN_ENTRY;
NSRange commandTextRange = [self getCommandTextRange];
[self.textView removeText:commandTextRange];
NSAttributedString *attrString = [self.outputFormatter formatInputText:newCommandText];
[self appendAttributedStringToTextStorage:attrString];
}
- (void)endCommandInProgressAtTimeout
{
//XT_DEF_SELNAME;
if (! [self.timedCommandState isTimedOut]) {
return;
}
NSAttributedString *attrStringNewline = [self.outputFormatter formatInputText:@"\n"];
[self appendAttributedStringToTextStorage:attrStringNewline];
}
//TODO !!! refactor logic?
- (void)mainThread_restoreCommandInProgressAtTimeout
{
//XT_DEF_SELNAME;
if (! [self.timedCommandState isCancelled]) {
self.timedCommandState = [XTTimedCommandState new];
return;
}
if (self.timedCommandState.command.length == 0) {
self.timedCommandState = [XTTimedCommandState new];
return;
}
NSString *usedCommand = self.timedCommandState.command;
unichar firstChar = [self.timedCommandState.command characterAtIndex:0];
if (firstChar != ZERO_WIDTH_SPACE_CHAR) {
NSMutableString *mutCommand = [NSMutableString stringWithString:self.timedCommandState.command];
[mutCommand insertString:ZERO_WIDTH_SPACE atIndex:0];
usedCommand = [NSString stringWithString:mutCommand];
//TODO !!! exp:
self.commandPromptPosition += 1;
}
NSAttributedString *attrString = [self.outputFormatter formatInputText:usedCommand];
[self appendAttributedStringToTextStorage:attrString];
self.timedCommandState = [XTTimedCommandState new];
}
- (void)mainThread_captureCommandInProgressAtTimeout
{
NSString *command = [self getCommandString];
[self setCommandInProgressAtTimeout:command];
[self.timedCommandState setTimedOut];
}
- (void)setCommandInProgressAtTimeout:(NSString *)command
{
//XT_DEF_SELNAME;
if (command.length >= 1) {
self.timedCommandState.command = command;
} else {
self.timedCommandState.command = nil;
}
NSUInteger minPos = [self commandPromptPosition];
self.timedCommandState.cursorOffsetFromEndOfCommand = [self.textView cursorOffsetFromEndOfTextMinPosition:minPos];
self.timedCommandState.selectedTextAffinity = [self.textView selectedTextAffinity];
}
- (NSString *)hardNewline
{
NSString *res;
if (self.htmlMode) {
res = @"
";
} else {
res = @"\n";
}
return res;
}
- (void)setColorsFromPrefsColor
{
self.textView.backgroundColor = [self.colorationHelper getOutputBackgroundColorForTextView];
[self.colorationHelper applyPrefsTextAndInputColors];
[self.colorationHelper updateLinkColors];
[self.colorationHelper updateTableColors];
[self updateCursorColor];
[self.textView setNeedsDisplay:YES];
//TODO why is this needed?:
[self scrollToBottom];
}
- (void)setColorsFromPrefAllowGameToSetColors
{
self.textView.backgroundColor = [self.colorationHelper getOutputBackgroundColorForTextView];
[self.colorationHelper applyTextAndInputColorsForAllowGameToSetColors];
[self.colorationHelper updateLinkColors];
[self.colorationHelper updateTableColors];
[self updateCursorColor];
[self.textView setNeedsDisplay:YES];
//TODO why is this needed?:
[self scrollToBottom];
}
- (void)setColorsFromBody
{
self.textView.backgroundColor = [self.colorationHelper getOutputBackgroundColorForTextView];
[self.colorationHelper applyBodyTextAndInputColorsForceApply:YES];
[self.colorationHelper applyBodyLinkColor];
[self updateCursorColor];
[self.textView setNeedsDisplay:YES];
//TODO why is this needed?:
[self scrollToBottom];
}
//========= Internal functions =======================================
- (void)updateCursorColor
{
[self.outputFormatter updateCursorColor];
}
- (BOOL)shouldAutoScrollToBottom
{
// main output area should always autoscroll to bottom of text when asked to
return YES;
}
- (NSRange)getCommandTextRange
{
NSUInteger minInsertionPoint = self.minInsertionPoint;
NSUInteger endOfOutputPosition = [self.textView endOfOutputPosition];
NSRange commandTextRange = NSMakeRange(minInsertionPoint, endOfOutputPosition - minInsertionPoint);
return commandTextRange;
}
- (void)noteEndOfOutput
{
//XT_DEF_SELNAME;
// find new starting pos of cmd prompt
NSUInteger oldPos = self.commandPromptPosition;
NSUInteger tempPos = [self.textView endOfOutputPosition];
NSUInteger newPos = (tempPos > 0 ? tempPos - 1 : 0);
if (newPos > oldPos) {
int brkpt = 1;
}
self.commandPromptPosition = newPos;
//XT_WARN_2(@"commandPromptPosition %lu -> %lu", oldPos, newPos);
}
- (BOOL)processTagTree
{
//XT_DEF_SELNAME;
//XT_TRACE_1(@"%lu entries", [self.formattingQueue count]);
if (self.activeTagBannerHandle != nil) {
//XT_WARN_1(@"activeTagBannerHandle != nil - continue with %lu elts in fmt queue", self.formattingQueue.count);
XTBannerTextHandler *bannerHandler = [self.gameWindowController bannerHandlerForHandle:self.activeTagBannerHandle];
self.currentTag = [bannerHandler processTagTreeFromMainOutput:self.currentTag parser:[self getOutputTextParser]];
// this will consume queue until or queue empty
}
if (self.statusLineMode == STATUS_LINE_MODE_STATUS && ! self.htmlMode) {
XTBannerTextHandler *bannerHandler = [self.gameWindowController getBannerHandlerForTradStatusLine];
self.currentTag = [bannerHandler processTagTreeFromMainOutput:self.currentTag parser:[self getOutputTextParser]];
// this will consume queue until or queue empty
}
BOOL reachedPaginationLimit = NO;
NSUInteger oldTextLength = self.textView.textStorage.length;
[self ensureWeHaveCurrentTag];
reachedPaginationLimit = [self processTags];
NSUInteger newTextLength = self.textView.textStorage.length;
[self trimScrollbackBuffer];
[self scrollToBottom];
NSUInteger textLengthAdded = newTextLength - oldTextLength;
if (textLengthAdded >= 1) {
[self noteEndOfOutput];
}
return reachedPaginationLimit;
}
- (BOOL)processFormattedElementQueue
{
XT_DEF_SELNAME;
BOOL reachedPaginationLimit = NO;
while (! [self.formattedElementQueue isEmpty] && ! reachedPaginationLimit) {
XTFormattedOutputElement *outputElement = [self.formattedElementQueue removeFirst];
if ([self isInTable]) {
if ([outputElement isTableEnd]) {
[self handleTableEnd];
} else {
[self addToTempFormattedElementQueueForTable:outputElement];
}
} else if ([outputElement isRegularOutputElement]) {
//XT_WARN_1(@"handling RegularOutputElement \"%@\"", outputElement.attributedString.string);
reachedPaginationLimit = [self processFormattedElementQueueRegularElementString:outputElement.attributedString];
} else if ([outputElement isTabElement]) {
reachedPaginationLimit = [self processFormattedElementQueueRegularElementString:outputElement.attributedString];
} else if ([outputElement isGameTitleElement]) {
[self processFormattedElementQueueGameTitleElement:outputElement];
} else if ([outputElement isBannerStartElement]) {
//XT_WARN_1(@"handling BannerStartElement id=\"%@\"", [((XTHtmlTagBanner *)outputElement.htmlTag) safeBannerId]);
[self handleBannerTagStart:outputElement];
} else if ([outputElement isStatusLineModeStart]) {
//XT_WARN_0(@"handling StatusLineModeStart");
[self handleStatusLineModeStart:outputElement];
} else if ([outputElement isStatusLineModeEnd]) {
//XT_WARN_0(@"handling StatusLineModeEnd");
if (! self.htmlMode) {
//XT_ERROR_0(@"isStatusLineModeEnd && ! self.htmlMode");
// nah, can happen at start of games
}
} else if ([outputElement isStatusLineModeSuppress]) {
//XT_WARN_0(@"handling StatusLineModeSuppress]");
_statusLineMode = STATUS_LINE_MODE_SUPPRESS; // don't do .statusLineMode, it'll loop things
} else if ([outputElement isClearWhitespaceBeforeOrAfterBlockLevelTagOutputElement]) {
[self clearWhitespaceBeforeOrAfterBlockLevelTagOutputElement];
} else if ([outputElement isBody]) {
[self handleBody:outputElement];
} else if ([outputElement isTableStart]) {
[self handleTableStart];
} else {
XT_ERROR_1(@"unexpected XTFormattedOutputElement %@", [outputElement elementTypeAsString]);
}
}
return reachedPaginationLimit;
}
- (void)processFormattedElementQueueGameTitleElement:(XTFormattedOutputElement *)outputElement
{
if ([outputElement.attributedString.string isEqualToString:@"{{clear}}"]) {
//TODO make element type for this case
self.gameTitle = [NSMutableString stringWithString:@""];
} else {
[self.gameTitle appendString:outputElement.attributedString.string];
}
}
- (void)flushPendingNewline
{
// only relevant for banners
}
//TODO move
- (void)exitingStatusLineMode:(XTFormattedOutputElement *)outputElement
{
//XT_DEF_SELNAME;
XTBannerTextHandler *bhTSL = [self.gameWindowController getBannerHandlerForTradStatusLine];
if (bhTSL != nil) {
[bhTSL flushTradStatusLineScoreString];
}
_statusLineMode = STATUS_LINE_MODE_MAIN; // don't do .statusLineMode, it'll loop things
}
- (void)handleBannerTagStart:(XTFormattedOutputElement *)outputElement
{
XT_DEF_SELNAME;
//TODO !!! exp for erudite missing space bug:
//[self.outputFormatter flushPendingWhitespace]; // ignore result value
[self flushFormattingQueue];
BOOL removeAllBanners = [outputElement.htmlTag hasAttribute:@"removeall"];
if (removeAllBanners) {
[self.gameWindowController bannerDeleteAll];
return;
}
NSString *tagId = [outputElement.htmlTag attributeAsString:@"id"];
if (tagId == nil || tagId.length == 0) {
tagId = @"xtads-id-less-banner";
XT_TRACE_0(@" has no id attribute - using a default id");
}
void *bannerHandle = [self.gameWindowController bannerHandleForTagId:tagId];
BOOL removeOneBanner = [outputElement.htmlTag hasAttribute:@"remove"];
if (removeOneBanner) {
if (bannerHandle != NULL) {
[self.gameWindowController bannerDelete:bannerHandle];
} else {
XT_WARN_1(@"Cannot remove non-existent banner with tagId %@", tagId);
}
return;
}
NSString *alignStr = [outputElement.htmlTag attributeAsString:@"align"];
NSInteger align = [self bannerAlignmentFrom:alignStr];
NSInteger sizeUnits = OS_BANNER_SIZE_ABS;
NSInteger size = 0;
BOOL sizeToContents = YES;
BOOL sizeAsPrevious = NO;
NSString *sizeAttrName = @"height";
if ((align == OS_BANNER_ALIGN_LEFT) || (align == OS_BANNER_ALIGN_RIGHT)) {
sizeAttrName = @"width";
}
NSString *sizeStr = [outputElement.htmlTag attributeAsString:sizeAttrName];
[self extractTagBannerSizeFrom:sizeStr
attrName:sizeAttrName
sizeToContents:&sizeToContents
sizeAsPrevious:&sizeAsPrevious
size:&size
sizeUnits:&sizeUnits];
NSInteger style = 0;
if ([outputElement.htmlTag hasAttribute:@"border"]) {
style |= OS_BANNER_STYLE_BORDER;
}
XTBannerTextHandler *bannerHandler;
if (bannerHandle == NULL) {
void *parent = 0; // parent is always"root banner", i.e. main output area
NSInteger where = OS_BANNER_LAST;
void *other = 0;
NSInteger wintype = OS_BANNER_TYPE_TEXT;
bannerHandle = [self.gameWindowController bannerCreate:parent
tagId:tagId
where:where
other:other
wintype:wintype
align:align
size:size
sizeUnits:sizeUnits
style:style
htmlMode:YES];
bannerHandler = [self.gameWindowController bannerHandlerForHandle:bannerHandle];
bannerHandler.wasInitiallySizedToPrevious = sizeAsPrevious;
bannerHandler.tagBannerNeedsSizeToContent = (sizeToContents || sizeAsPrevious);
// keep this value, so that tag banner gets resized to current contents if necessary
//TODO very clumsy to do it this way...
bannerHandler.mainTextHandler = self;
} else {
bannerHandler = [self.gameWindowController bannerHandlerForHandle:bannerHandle];
//XT_WARN_0(@"call bannerHandler synchClear");
[bannerHandler synchClear];
//TODO? don't resize if size not changed
[self.gameWindowController tagBannerReconfigure:bannerHandle
align:align
sizeToContents:sizeToContents
sizeAsPrevious:sizeAsPrevious
size:size
sizeUnits:sizeUnits
style:style];
}
bannerHandler.hadUnspecifiedSizeLastTime = sizeToContents;
bannerHandler.hadPreviousSizeLastTime = sizeAsPrevious;
// Needed for a weird-ass special case :-(
self.activeTagBannerHandle = bannerHandle;
//XT_WARN_1(@"self.formattingQueue has %lu entries", self.formattingQueue.count);
self.currentTag = [bannerHandler processTagTreeFromMainOutput:self.currentTag parser:[self getOutputTextParser]];
// this will consume queue until or queue empty
}
- (void)handleStatusLineModeStart:(XTFormattedOutputElement *)outputElement
{
XT_DEF_SELNAME;
//XT_WARN_0(@"");
if (self.htmlMode) {
// let handling take care of it
//XT_WARN_0(@"let handling take care of it");
return;
}
//XT_WARN_0(@"handling it here");
[self.gameWindowController createBannerForTradStatusLine]; // ... if none exists already
XTBannerTextHandler *bhTSL = [self.gameWindowController getBannerHandlerForTradStatusLine];
//TODO mv into this class?
BOOL switchingToStatusLineMode = (self.statusLineMode != STATUS_LINE_MODE_STATUS);
_statusLineMode = STATUS_LINE_MODE_STATUS; // don't do .statusLineMode, it'll loop things
if (switchingToStatusLineMode) {
if (bhTSL != nil) {
[bhTSL executeClear];
[bhTSL resetForTradStatusLine];
NSMutableArray *params = [NSMutableArray arrayWithCapacity:2];
params[0] = [NSNumber numberWithBool:YES]; // input param
[bhTSL mainThread_pumpOutputText:params];
} else {
XT_ERROR_0(@"bhTSL == nil");
}
} else {
int brkpt = 1;
}
self.currentTag = [bhTSL processTagTreeFromMainOutput:self.currentTag parser:[self getOutputTextParser]];
// this will consume queue until or queue empty
}
- (void)extractTagBannerSizeFrom:(NSString *)string
attrName:(NSString *)attrName
sizeToContents:(BOOL *)sizeToContents
sizeAsPrevious:(BOOL *)sizeAsPrevious
size:(NSInteger *)size
sizeUnits:(NSInteger *)sizeUnits
{
XT_DEF_SELNAME;
*sizeToContents = YES;
*sizeAsPrevious = NO;
if ([string length] >= 1) {
string = [string lowercaseString];
if ([string isEqualToString:@"previous"]) {
*sizeToContents = NO;
*sizeAsPrevious = YES;
} else if ([string hasSuffix:@"%"]) {
NSUInteger idxPctSign = string.length - 1;
NSString *numPrefix = [string substringToIndex:idxPctSign];
NSScanner *scanner = [NSScanner scannerWithString:numPrefix];
NSInteger tempSize;
BOOL found = [scanner scanInteger:&tempSize];
if (found && [scanner isAtEnd] && tempSize >= 0 && tempSize <= 100) {
*size = tempSize;
*sizeUnits = OS_BANNER_SIZE_PCT;
*sizeToContents = NO;
*sizeAsPrevious = NO;
} else {
XT_WARN_2(@"illegal %* attribute \"%@\" - defaulting to content size", attrName, string);
// keep default "size to content"
}
} else {
NSScanner *scanner = [NSScanner scannerWithString:string];
NSInteger tempSize;
BOOL found = [scanner scanInteger:&tempSize];
if (found && [scanner isAtEnd] && tempSize >= 0) {
*size = tempSize;
*sizeUnits = OS_BANNER_SIZE_PIXELS;
*sizeToContents = NO;
*sizeAsPrevious = NO;
} else {
XT_WARN_2(@"illegal %* attribute \"%@\" - defaulting to content size", attrName, string);
// keep default "size to content"
}
}
}
}
- (NSInteger)bannerAlignmentFrom:(NSString *)alignStr
{
XT_DEF_SELNAME;
NSInteger res = OS_BANNER_ALIGN_TOP;
if (alignStr != nil) {
NSString *alignStrLc = [alignStr lowercaseString];
if ([alignStrLc isEqualToString:@"top"]) {
res = OS_BANNER_ALIGN_TOP;
} else if ([alignStrLc isEqualToString:@"bottom"]) {
res = OS_BANNER_ALIGN_BOTTOM;
} else if ([alignStrLc isEqualToString:@"left"]) {
res = OS_BANNER_ALIGN_LEFT;
} else if ([alignStrLc isEqualToString:@"right"]) {
res = OS_BANNER_ALIGN_RIGHT;
} else {
XT_WARN_1(@"unknown alignment %@ - using default TOP", alignStr);
res = OS_BANNER_ALIGN_TOP;
}
}
return res;
}
- (void)trimScrollbackBuffer
{
XT_DEF_SELNAME;
XTPrefs *prefs = [XTPrefs prefs];
if (! prefs.limitScrollbackBufferSize.value.boolValue) {
return;
}
NSUInteger scrollbackBufferSize = 1000 * prefs.scrollbackBufferSizeInKBs.value.unsignedIntegerValue;
NSTextStorage *ts = [self.textView textStorage];
NSUInteger tsSize = ts.length;
XT_TRACE_1(@"tsSize=%lu", tsSize);
if (tsSize > scrollbackBufferSize) {
NSUInteger excess = tsSize - scrollbackBufferSize;
NSUInteger deleteBlockSize = 20000; // so we only delete if in excess by a goodish amount
if (excess > deleteBlockSize) {
CGFloat trimmedTextViewHeight = [self.textView removeTextFromStart:excess];
self.maxTextViewHeightBeforePagination -= trimmedTextViewHeight;
XT_TRACE_1(@"maxTextViewHeightBeforePagination", self.maxTextViewHeightBeforePagination);
}
}
}
//------- App. level notifications -------
- (void)setupReceptionOfAppLevelNotifications
{
XT_TRACE_ENTRY;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotifyTextLinkClicked:)
name:XTadsNotifyTextLinkClicked
object:nil]; // nil means "for any sender"
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleSetFocusToMainOutput:)
name:XTadsNotifySetFocusToMainOutput
object:nil]; // nil means "for any sender"
}
- (void)teardownReceptionOfAppLevelNotifications
{
XT_TRACE_ENTRY;
[[NSNotificationCenter defaultCenter] removeObserver:self
name:XTadsNotifyTextLinkClicked
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:XTadsNotifySetFocusToMainOutput
object:nil];
}
- (void)handleNotifyTextLinkClicked:(NSNotification *)notification
{
NSString *linkText = notification.userInfo[XTadsNotificationUserInfoKeyLink];
NSNumber *tempCharIndex = notification.userInfo[XTadsNotificationUserInfoKeyLinkCharIndex];
NSUInteger charIndex = tempCharIndex.unsignedIntegerValue;
[self handleCommandLinkClicked:linkText atIndex:charIndex];
}
- (void)handleSetFocusToMainOutput:(NSNotification *)notification
{
XT_TRACE_ENTRY;
// Transfer focus back to main output view
[[self.textView window] makeFirstResponder:self.textView];
[self moveCursorToEndOfOutputPosition];
}
- (void)configureViews
{
XT_TRACE_ENTRY;
CGSize oldMainOutputAreaSize = self.scrollView.frame.size;
[self tearDownLayoutViews];
NSView *overallView = [self internalRebuildViewHierarchy];
if (overallView == nil) {
// can happen when closing game window
return;
}
[self.rootBannerContainerView addSubview:overallView];
// Make overallView fill all of its superview:
[XTViewLayoutUtils addEdgeConstraint:NSLayoutAttributeLeft superview:self.rootBannerContainerView subview:overallView];
[XTViewLayoutUtils addEdgeConstraint:NSLayoutAttributeRight superview:self.rootBannerContainerView subview:overallView];
[XTViewLayoutUtils addEdgeConstraint:NSLayoutAttributeTop superview:self.rootBannerContainerView subview:overallView];
[XTViewLayoutUtils addEdgeConstraint:NSLayoutAttributeBottom superview:self.rootBannerContainerView subview:overallView];
// Make sure view frames are up to date, for pagination calcs
// See https://www.objc.io/issues/3-views/advanced-auto-layout-toolbox/
[self.rootBannerContainerView layoutSubtreeIfNeeded];
CGSize newMainOutputAreaSize = self.scrollView.frame.size;
BOOL changedMainOutputAreaSize = ((newMainOutputAreaSize.width != oldMainOutputAreaSize.width) ||
(newMainOutputAreaSize.height != oldMainOutputAreaSize.height));
[self recalcDynamicTabStops:changedMainOutputAreaSize];
[XTNotifications notifySetFocusToMainOutputView:self];
}
@end