//
// 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"
@interface XTMainTextHandler ()
@property NSTimeInterval totalTimeInAppendAttributedStringToTextStorage;
@property BOOL afterNewlineAfterCommand;
@property NSUInteger countNewlinesAfterCommand;
@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:@""];
_totalTimeInAppendAttributedStringToTextStorage = 0.0;
_afterNewlineAfterCommand = NO;
_countNewlinesAfterCommand = 0;
[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 == 1) {
int brkpt = 1;
}
[[self getOutputTextParser] appendTagToCurrentContainer:tag];
}
- (void)setStatusLineModeNow:(NSUInteger)statusLineMode
{
_statusLineMode = statusLineMode;
}
- (NSUInteger)statusLineMode {
return _statusLineMode;
}
//TODO refactor wrt base & banner classes
- (void)removeHandler
{
XT_DEF_SELNAME;
XT_TRACE_0(@"");
[self.outputFormatter teardown];
self.outputFormatter = nil;
[self.textView teardown];
self.textView = nil;
[self teardownReceptionOfAppLevelNotifications];
self.scrollView = nil;
self.gameWindowController = nil;
self.gameTitle = nil;
self.commandHistory = nil;
self.activeTagBannerHandle = nil;
[self.outputTextParserPlain teardown];
[self.outputTextParserHtml teardown];
}
+ (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;
}
//TODO refactor wrt base & banner classes
- (void)resetToDefaults
{
// called when game file loads and starts
self.htmlMode = NO;
self.hiliteMode = NO;
[self setNonstopMode:NO];
self.statusLineMode = STATUS_LINE_MODE_MAIN;
self.afterNewlineAfterCommand = 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
{
[super resetForNextCommand];
[self.textStorageBatcher reset];
[self ensureInputFontIsInEffect];
self.afterNewlineAfterCommand = NO;
}
- (void)resetForGameHasEndedMsg
{
[[self getOutputTextParser] flush];
[self.outputTextParserHtml resetForGameHasEndedMsg];
[self.outputTextParserPlain resetForNextCommand];
[self.outputFormatter resetFlags];
[self mainThread_noteStartOfPagination];
[self clearPaginationState];
[self.textStorageBatcher reset];
self.afterNewlineAfterCommand = 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;
}
- (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
{
NSRange range = [self getCommandTextRange];
NSString *command = [self.textView stringInRange:range];
[self.commandHistory appendCommand:command];
returnValue[0] = command;
}
- (void)clearText
{
//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;
//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.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.htmlMode) {
if ([string isEqualToString:@"\n"]) {
if (self.countNewlinesAfterCommand >= 1) {
self.countNewlinesAfterCommand += 1;
}
//if (! self.afterNewlineAfterCommand) {
if (self.countNewlinesAfterCommand != 3) {
[[self getOutputTextParser] parse:string];
}
XT_WARN_1(@"countNewlinesAfterCommand=%lu", self.countNewlinesAfterCommand); //TODO !!! rm
} else {
[[self getOutputTextParser] parse:string];
self.countNewlinesAfterCommand = 0;
}
//self.afterNewlineAfterCommand = 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"]) {
//if (self.afterNewlineAfterCommand) {
if (self.countNewlinesAfterCommand >= 1) {
XT_DEF_SELNAME;
XT_ERROR_0(@"afterNewlineAfterCommand should not be true here");
int brkpt = 1;
}
//self.afterNewlineAfterCommand = YES;
self.countNewlinesAfterCommand = 1;
XT_WARN_1(@"countNewlinesAfterCommand=%lu", self.countNewlinesAfterCommand); //TODO !!! rm
} 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)];
}
- (BOOL)cursorIsInCommand
{
BOOL res = ([self insertionPoint] >= [self minInsertionPoint]);
return res;
}
- (BOOL)cursorIsAtMinInsertionPosition
{
BOOL res = ([self insertionPoint] == [self minInsertionPoint]);
return res;
}
- (BOOL)cursorIsInCommandButNotAtMinInsertionPosition
{
BOOL res = ([self insertionPoint] > [self minInsertionPoint]);
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)setColorsFromPrefs
{
//TODO when text/bg col set by game
//TODO when/not xtads allows game to set text/bg col
self.textView.backgroundColor = self.prefs.outputAreaBackgroundColor.value;
//TODO? cursor color
[self.textView setNeedsDisplay:YES];
//TODO why is this needed?:
[self scrollToBottom];
}
//========= Internal functions =======================================
- (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
{
// find new starting pos of cmd prompt
NSUInteger tempPos = [self.textView endOfOutputPosition];
self.commandPromptPosition = (tempPos > 0 ? tempPos - 1 : 0);
}
- (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;
[self ensureWeHaveCurrentTag];
reachedPaginationLimit = [self processTags];
[self trimScrollbackBuffer];
[self scrollToBottom];
[self noteEndOfOutput];
return reachedPaginationLimit;
}
- (BOOL)processFormattedElementQueue
{
XT_DEF_SELNAME;
BOOL reachedPaginationLimit = NO;
while (! [self.formattedElementQueue isEmpty] && ! reachedPaginationLimit) {
XTFormattedOutputElement *outputElement = [self.formattedElementQueue removeFirst];
if ([outputElement isRegularOutputElement]) {
//XT_WARN_1(@"handling RegularOutputElement \"%@\"", outputElement.attributedString.string);
BOOL overBatchLimit = [self.textStorageBatcher append:outputElement.attributedString];
if (overBatchLimit) {
// *batch* limit (not pagination limit) - output what we can until hitting pagination limit
reachedPaginationLimit = [self processBatchedOutputUntilReachedPaginationLimit];
if (! reachedPaginationLimit) {
[self.textStorageBatcher reset];
}
}
} else if ([outputElement isTabElement]) {
NSArray *array = [NSArray arrayWithObject:outputElement.attributedString];
BOOL overBatchLimit = [self.textStorageBatcher appendArray:array];
if (overBatchLimit) {
// *batch* limit (not pagination limit) - output what we can until hitting pagination limit
reachedPaginationLimit = [self processBatchedOutputUntilReachedPaginationLimit];
if (! reachedPaginationLimit) {
[self.textStorageBatcher reset];
} else {
int brkpt = 1;
}
}
} else if ([outputElement isGameTitleElement]) {
if ([outputElement.attributedString.string isEqualToString:@"{{clear}}"]) {
//TODO make element type for this case
self.gameTitle = [NSMutableString stringWithString:@""];
} else {
[self.gameTitle appendString:outputElement.attributedString.string];
}
} 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 {
XT_ERROR_1(@"unexpected XTFormattedOutputElement %@", [outputElement elementTypeAsString]);
}
}
return reachedPaginationLimit;
}
- (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];
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;
[self.gameWindowController bannerSetHtmlMode:bannerHandle on:YES];
} 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);
[bannerHandler noteStartedFromHtmlTag];
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
return;
}
[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