TwoTerm/Views/EmulatorView.mm

964 lines
22 KiB
Plaintext

//
// EmulatorView.m
// 2Term
//
// Created by Kelvin Sherlock on 7/3/2010.
// Copyright 2010 __MyCompanyName__. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#include <termios.h>
#include <sys/ioctl.h>
#import "EmulatorView.h"
#import "CharacterGenerator.h"
//#import "VT52.h"
//#import "VT100.h"
#include "OutputChannel.h"
#import "ScanLineFilter.h"
@implementation EmulatorView (Cursor)
-(void)startCursorTimer
{
// timers must be added/removed from the same thread.
if (_cursorTimer) return;
if ([NSThread isMainThread])
{
_cursorOn = NO;
_cursorTimer = [[NSTimer alloc] initWithFireDate: [NSDate date]
interval: 0.5
target: self
selector: @selector(cursorTimer:)
userInfo: nil
repeats: YES ];
[[NSRunLoop currentRunLoop] addTimer: _cursorTimer forMode: NSDefaultRunLoopMode];
}
else
{
[self performSelectorOnMainThread: _cmd withObject: nil waitUntilDone: NO];
}
}
-(void)stopCursorTimer
{
if ([NSThread isMainThread])
{
[_cursorTimer invalidate];
[_cursorTimer release];
_cursorTimer = nil;
iRect r(_screen.cursor(), iSize(1,1));
[self invalidateIRect: r];
}
else
{
[self performSelectorOnMainThread: _cmd withObject: nil waitUntilDone: NO];
}
}
-(void)cursorTimer: (NSTimer *)timer
{
if (_cursorType == Screen::CursorTypeNone) return;
_screen.lock();
_cursorOn = !_cursorOn;
iRect r(_screen.cursor(), iSize(1,1));
[self invalidateIRect: r];
_screen.unlock();
}
-(void)setCursorType: (unsigned)cursorType
{
if (_cursorType == cursorType) return;
_cursorType = cursorType;
iRect r(_screen.cursor(), iSize(1,1));
[self invalidateIRect: r];
}
-(unsigned)cursorType
{
return _cursorType;
}
#if 0
dispatch_async(dispatch_get_main_queue(), ^(){
if (_cursorType == cursorType) return;
unsigned char c;
switch (cursorType) {
default:
case Screen::CursorTypeUnderscore: c = '_'; break;
case Screen::CursorTypePipe: c = '|'; break;
case Screen::CursorTypeBlock: c = 0x80; break;
}
[_cursorImg release];
_cursorType = cursorType;
_cursorImg = [[_charGen imageForCharacter: c] retain];
iRect r(_screen.cursor(), iSize(1,1));
[self invalidateIRect: r];
});
#endif
@end
@implementation EmulatorView
@synthesize fd = _fd;
//@synthesize cursorType = _cursorType;
@synthesize emulator = _emulator;
@synthesize foregroundColor = _foregroundColor;
@synthesize backgroundColor = _backgroundColor;
@synthesize scanLines = _scanLines;
//@synthesize cursorType = _cursorType;
@dynamic cursorType;
#pragma mark -
#pragma mark properties
-(int)fd {
return _fd;
}
-(void)setFd: (int)fd
{
_fd = fd;
_screen.setFD(fd);
}
#pragma mark -
-(void)awakeFromNib
{
NSSize size;
_charWidth = 7;
_charHeight = 16;
_paddingLeft = 8;
_paddingTop = 8;
_paddingBottom = 8;
//_foregroundColor = [[NSColor greenColor] retain];
//_backgroundColor = [[NSColor blackColor] retain];
_boldColor = [[NSColor redColor] retain];
//_foregroundColor = [[NSColor whiteColor] retain];
//_backgroundColor = [[NSColor blueColor] retain];
_screen.setFD(_fd);
_screen.setView(self);
_charGen = [[CharacterGenerator generator] retain];
_cursorType = Screen::CursorTypeUnderscore;
_cursors[Screen::CursorTypeNone] = nil;
_cursors[Screen::CursorTypeUnderscore] = [[_charGen imageForCharacter: '_'] retain];
_cursors[Screen::CursorTypePipe] = [[_charGen imageForCharacter: '|'] retain];
_cursors[Screen::CursorTypeBlock] = [[_charGen imageForCharacter: 0x80] retain];
size = [_charGen characterSize];
_charWidth = size.width;
_charHeight = size.height;
// enable drag+drop for files/urls.
[self registerForDraggedTypes:[NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType , nil]];
}
-(void)setBackgroundColor:(NSColor *)color
{
if (_backgroundColor == color) return;
[_backgroundColor release];
_backgroundColor = [color retain];
[self setNeedsDisplay: YES];
}
-(void)setForegroundColor:(NSColor *)color
{
if (_foregroundColor == color) return;
[_foregroundColor release];
_foregroundColor = [color retain];
[self setNeedsDisplay: YES];
}
-(void)setScanLines:(BOOL)scanLines
{
if (_scanLines == scanLines) return;
_scanLines = scanLines;
if (_scanLines)
{
NSMutableArray *filters;
CIFilter *filter;
[self setWantsLayer: YES];
filters = [NSMutableArray arrayWithCapacity: 3];
//add the scanlines
filter = [[ScanLineFilter new] autorelease];
[filter setValue: [NSNumber numberWithFloat: 1.0] forKey: @"inputDarken"];
[filter setValue: [NSNumber numberWithFloat: 0.025] forKey: @"inputLighten"];
[filters addObject: filter];
//blur it a bit...
filter = [CIFilter filterWithName: @"CIGaussianBlur"];
[filter setDefaults];
[filter setValue: [NSNumber numberWithFloat: 0.33] forKey: @"inputRadius"];
[filters addObject: filter];
[self setContentFilters: filters];
}
else
{
[self setContentFilters: @[]];
}
}
-(BOOL)isFlipped
{
return YES;
}
-(BOOL)isOpaque {
return NO;
}
-(void)viewDidMoveToWindow
{
[self becomeFirstResponder];
[self startCursorTimer];
/*
[[self window] display];
[[self window] setHasShadow: NO];
[[self window] setHasShadow: YES];
*/
}
-(void)viewDidMoveToSuperview
{
[self becomeFirstResponder];
}
-(BOOL)acceptsFirstResponder
{
return YES;
}
-(BOOL)resignFirstResponder {
return [super resignFirstResponder];
}
-(void)drawRect:(NSRect)dirtyRect
{
//NSLog(@"drawRect:");
//NSRect bounds = [self bounds];
NSRect screenRect = dirtyRect;
unsigned x, y;
NSColor *currentFront;
NSColor *currentBack;
BOOL setFront = YES;
screenRect.origin.x -= _paddingLeft;
screenRect.origin.y -= _paddingTop;
if (screenRect.origin.x < 0)
{
screenRect.size.width -= screenRect.origin.x;
screenRect.origin.x = 0;
}
if (screenRect.origin.y < 0)
{
screenRect.size.width -= screenRect.origin.y;
screenRect.origin.y = 0;
}
int minX = floor(screenRect.origin.x / _charWidth);
int maxX = ceil((screenRect.origin.x + screenRect.size.width) / _charWidth);
int minY = floor(screenRect.origin.y / _charHeight);
int maxY = ceil((screenRect.origin.y + screenRect.size.height) / _charHeight);
// x/y are 0-indexed here.
maxY = std::min(_screen.height() - 1, maxY);
maxX = std::min(_screen.width() - 1, maxX);
[_backgroundColor setFill];
NSRectFill(dirtyRect);
[_foregroundColor setFill];
//currentFront = _foregroundColor;
//currentBack = _backgroundColor;
_screen.lock();
for (x = minX; x <= maxX; ++x)
{
for (y = minY; y <= maxY; ++y)
{
NSRect charRect = NSMakeRect(_paddingLeft + x * _charWidth, _paddingTop + y *_charHeight, _charWidth, _charHeight);
//NSImage *img;
char_info ci = _screen.getc(iPoint(x, y));
unsigned flag = ci.flag;
uint8_t c = ci.c;
// todo -- check flags to determine fg/bg color, etc.
// need to draw background individually....
currentBack = _backgroundColor;
currentFront = _foregroundColor;
if (flag & Screen::FlagBold)
currentFront = _boldColor;
//img = [_charGen imageForCharacter: c];
if (flag & Screen::FlagInverse)
{
// mouse text actually requires mouse text and inverse to be on.
if (flag & Screen::FlagMouseText)
{
if (c >= '@' && c <= '_') c |= 0x80;
}
else
{
std::swap(currentBack, currentFront);
}
}
if (_cursorType == Screen::CursorTypeBlock && _cursorOn && _screen.cursor() == iPoint(x, y)) {
std::swap(currentBack, currentFront);
}
if (currentBack != _backgroundColor)
{
[currentBack setFill];
NSRectFill(charRect);
setFront = YES;
}
if (_foregroundColor != currentFront) setFront = YES;
if (setFront) [currentFront setFill];
// need to apply the scanline filter here.
[_charGen drawCharacter: c
atPoint: NSMakePoint(_paddingLeft + x * _charWidth, _paddingTop + y * _charHeight)];
// strikethrough -- draw a centered line.
if (flag & Screen::FlagStrike)
{
NSRectFill(NSMakeRect(_paddingLeft + x * _charWidth,
_paddingTop + y * _charHeight + floor(_charHeight / 2) - 1,
_charWidth,
2));
}
// underscore -- draw a bottom line. (eg, ``_'')
if (flag & Screen::FlagUnderscore)
{
NSRectFill(NSMakeRect(_paddingLeft + x * _charWidth,
_paddingTop + y * _charHeight + _charHeight - 2,
_charWidth,
2));
}
}
}
// cursor.
iPoint cursor = _screen.cursor();
if (_cursorOn && iRect(minX, minY, maxX - minX, maxY - minY).contains(cursor) && _cursorType != Screen::CursorTypeBlock)
{
NSRect charRect = NSMakeRect(_paddingLeft + cursor.x * _charWidth, _paddingTop + cursor.y *_charHeight, _charWidth, _charHeight);
[_foregroundColor setFill];
NSImage *img = _cursors[_cursorType];
NSCompositingOperation op = NSCompositingOperationCopy;
//if (_cursorType == Screen::CursorTypeBlock) op = NSCompositingOperationXOR;
[img drawInRect: charRect
fromRect: NSZeroRect
operation: op
fraction: 1.0
respectFlipped: YES
hints: nil];
}
_screen.unlock();
//[_scanLine setFill];
//NSRectFillUsingOperation(screenRect, NSCompositeSourceOver);
//NSRectFill(screenRect);
}
-(void)dealloc
{
close(_fd);
[_foregroundColor release];
[_backgroundColor release];
[_emulator release];
//[_cursorImg release];
for (int i = 0; i < 4; ++i) [_cursors[i] release];
[super dealloc];
}
-(void)keyDown:(NSEvent *)theEvent
{
//NSLog(@"keyDown:");
if (_fd < 0) return;
OutputChannel channel(_fd);
iRect updateRect; // should be nil but whatever...
// todo -- after _fd closes, need to block further activity.
[NSCursor setHiddenUntilMouseMoves: YES];
_screen.beginUpdate();
[_emulator keyDown: theEvent screen: &_screen output: &channel];
updateRect = _screen.endUpdate();
[self invalidateIRect: updateRect];
}
-(void)autoTypeText:(NSString *)text
{
if (_fd < 0) return;
NSData *data = [text dataUsingEncoding: NSASCIIStringEncoding allowLossyConversion: YES];
NSUInteger length = [data length];
OutputChannel channel(_fd);
if (!length) return;
// bad form to write directly rather than going through Emulator object?
channel.write([data bytes], length);
}
-(void)childBegan {
//[self startCursorTimer];
}
-(void)childFinished:(int)status {
// called from other thread.
//NSLog(@"[process complete]");
dispatch_async(dispatch_get_main_queue(), ^(){
iRect updateRect;
[self setCursorType: Screen::CursorTypeNone];
[self stopCursorTimer];
//_screen.setCursorType(Screen::CursorTypeNone);
#if 0
_screen.beginUpdate();
_screen.setX(0);
_screen.incrementY();
for (const char *cp = "[Process completed]"; *cp; ++cp)
{
_screen.putc(*cp);
}
updateRect = _screen.endUpdate();
[self invalidateIRect: updateRect];
#endif
//[_emulator writeLine: @"[Process completed]"];
});
}
-(IBAction)clearDebugData:(id)sender {
_debug_buffer.clear();
}
-(IBAction)copyDebugData:(id)sender {
/* copy _debug_data to clipboard */
std::vector<uint8_t> bytes = _debug_buffer.read();
std::string ascii;
std::string hex;
ascii.reserve(bytes.size());
hex.reserve(bytes.size()*3);
std::transform(bytes.begin(), bytes.end(), std::back_inserter(ascii), [](uint8_t c){
if (isascii(c) && isprint(c)) return (char)c;
return (char)'.';
});
for (uint8_t c : bytes) {
constexpr const static char hh[] = "0123456789abcdef";
hex.push_back(hh[c >> 4]);
hex.push_back(hh[c & 0x0f]);
hex.push_back(' ');
}
std::string rv;
int offset = 0;
while (offset < bytes.size()) {
size_t max = bytes.size() - offset;
if (max > 16) max = 16;
rv.append(hex.data() + offset * 3, max * 3);
if (max < 16) rv.append((16 - max) * 3, ' ');
rv.push_back(' ');
rv.append(ascii.data() + offset, max);
rv.push_back('\n');
offset += max;
}
NSPasteboard *pb;
pb = [NSPasteboard generalPasteboard];
[pb clearContents];
[pb setData: [NSData dataWithBytes: rv.data() length: rv.length()] forType: NSStringPboardType];
}
-(void)processData: (uint8_t *)buffer size:(size_t)size {
typedef void (*ProcessCharFX)(id, SEL, uint8_t, Screen *, OutputChannel *);
OutputChannel channel(_fd);
iRect updateRect;
_debug_buffer.write(buffer, size);
if ([_emulator respondsToSelector: @selector(processData:length:screen:output:)]) {
@autoreleasepool {
_screen.beginUpdate();
[_emulator processData: buffer length: size screen: &_screen output: &channel];
updateRect = _screen.endUpdate();
dispatch_async(dispatch_get_main_queue(), ^(){
[self invalidateIRect: updateRect];
});
}
return;
}
@autoreleasepool {
SEL cmd = @selector(processCharacter: screen: output:);
ProcessCharFX fx = (ProcessCharFX)[_emulator methodForSelector: cmd];
_screen.beginUpdate();
for (unsigned i = 0; i < size; ++i)
{
fx(_emulator, cmd, buffer[i], &_screen, &channel);
}
updateRect = _screen.endUpdate();
dispatch_async(dispatch_get_main_queue(), ^(){
[self invalidateIRect: updateRect];
});
}
}
// should be done in the main thread.
-(void)invalidateIRect: (iRect)updateRect
{
//NSLog(@"invalidateIRect");
NSRect rect;
if (updateRect.size.width <= 0 || updateRect.size.height <= 0) return;
rect.origin.x = updateRect.origin.x;
rect.origin.y = updateRect.origin.y;
rect.size.width = updateRect.size.width;
rect.size.height = updateRect.size.height;
rect.origin.x *= _charWidth;
rect.origin.y *= _charHeight;
rect.size.width *= _charWidth;
rect.size.height *= _charHeight;
rect.origin.x += _paddingLeft;
rect.origin.y += _paddingTop;
dispatch_async(dispatch_get_main_queue(), ^(){
[self setNeedsDisplayInRect: rect];
//[self display];
});
//[self setNeedsDisplayInRect: rect];
//[self display];
}
-(void)resizeTo: (iSize)size animated: (BOOL)animated
{
NSWindow *window = [self window];
NSRect bounds = [self bounds];
NSSize newSize;
NSRect wframe = [window frame];
// TODO -- left/right padding...
newSize.width = size.width * _charWidth + _paddingLeft * 2;
newSize.height = size.height * _charHeight + _paddingTop + _paddingBottom;
// best case -- no change.
if (NSEqualSizes(newSize, bounds.size)) return;
// ok, change needed.
wframe.origin.y -= (newSize.height - bounds.size.height);
wframe.size.height += newSize.height - bounds.size.height;
wframe.size.width += newSize.width - bounds.size.width;
_inResizeTo = YES;
[window setFrame: wframe display: YES animate: animated];
_inResizeTo = NO;
}
-(void)resizeTo: (iSize)size
{
[self resizeTo: size animated: YES];
}
#if 0
-(void)viewWillStartLiveResize
{
//NSLog(@"%s", sel_getName(_cmd));
}
#endif
-(void)viewDidEndLiveResize
{
//NSLog(@"%s", sel_getName(_cmd));
[super viewDidEndLiveResize];
[self setNeedsDisplay: YES];
}
/*
* inLiveResize indicates the user is resizing the window -or- the -(void)setWindowFrame: animated: YES.
* non-live resize indicates zooming or resizeTo w/o animation.
*
*
*/
-(void)setFrame:(NSRect)frameRect
{
//NSLog(@"%s", sel_getName(_cmd));
BOOL inLiveResize = [self inLiveResize];
[super setFrame: frameRect];
if (inLiveResize && _inResizeTo)
{
return;
}
if (/* inLiveResize */ YES)
{
// user is resizing window.
// or zoom.
unsigned width = floor((frameRect.size.width - _paddingLeft * 2) / _charWidth);
unsigned height = floor((frameRect.size.height - _paddingTop - _paddingBottom) / _charHeight);
_screen.lock();
_screen.setSize(width, height, false);
_screen.unlock();
return;
}
}
#pragma mark -
#pragma mark IBActions
-(BOOL)validateUserInterfaceItem: (id <NSValidatedUserInterfaceItem>)anItem {
SEL cmd = [anItem action];
if (cmd == @selector(paste:)) {
return _fd >= 1;
}
if (cmd == @selector(copy:)) return NO;
if (cmd == @selector(copyDebugData:)) return YES;
if (cmd == @selector(clearDebugData:)) return YES;
return NO;
//return [super validateUserInterfaceItem: anItem];
}
//-(BOOL)validateUserInterfaceItem:
-(IBAction)paste: (id)sender
{
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
NSArray *classArray = [NSArray arrayWithObject:[NSString class]];
NSDictionary *options = [NSDictionary dictionary];
if (_fd < 0) return;
BOOL ok = [pasteboard canReadObjectForClasses:classArray options:options];
if (ok)
{
NSArray *objectsToPaste = [pasteboard readObjectsForClasses:classArray options:options];
NSString *string = [objectsToPaste objectAtIndex: 0];
//NSLog(@"%@", objectsToPaste);
[self autoTypeText: string];
}
}
-(IBAction)copy: (id)sender
{
}
#pragma mark -
#pragma mark Drag/Drop
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSPasteboard *pboard;
pboard = [sender draggingPasteboard];
NSArray *types = [pboard types];
if ([types containsObject: NSFilenamesPboardType]) return NSDragOperationCopy;
if ([types containsObject: NSURLPboardType]) return NSDragOperationCopy;
return NSDragOperationNone;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
NSArray *types;
sourceDragMask = [sender draggingSourceOperationMask];
pboard = [sender draggingPasteboard];
if (_fd < 0) return NO;
types = [pboard types];
if ([types containsObject: NSFilenamesPboardType])
{
NSArray *array = [pboard propertyListForType: NSFilenamesPboardType];
NSString *string = (NSString *)[array objectAtIndex: 0];
string = [string stringByReplacingOccurrencesOfString: @"\\" withString: @"\\\\"];
string = [string stringByReplacingOccurrencesOfString: @" " withString: @"\\ "];
[self autoTypeText: string];
//NSArray *array = [pboard propertyListForType: NSFilenamesPboardType];
//NSLog(@"%@", [array class]);
//NSLog(@"%@", [pboard propertyListForType: NSFilenamesPboardType]);
return YES;
}
if ([types containsObject: NSURLPboardType])
{
NSArray *array = [pboard propertyListForType: NSURLPboardType];
NSObject *object = (NSObject *)[array objectAtIndex: 0];
if ([object isKindOfClass: [NSString class]])
{
[self autoTypeText: (NSString *)object];
return YES;
}
if ([object isKindOfClass: [NSURL class]])
{
[self autoTypeText: [(NSURL *)object absoluteString]];
return YES;
}
// if file://, use the pathname?
//NSLog(@"%@", [array class]);
//NSLog(@"%@", [pboard propertyListForType: NSURLPboardType]);
}
return NO;
}
@end
void ViewScreen::setSize(unsigned width, unsigned height)
{
setSize(width, height, true);
}
void ViewScreen::setSize(unsigned width, unsigned height, bool resizeView)
{
//
struct winsize ws;
ws.ws_row = height;
ws.ws_col = width;
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
Screen::setSize(width, height);
ioctl(_fd, TIOCSWINSZ, &ws);
if (resizeView)
{
[_view resizeTo: iSize(width, height) animated: YES];
}
}
void ViewScreen::setCursorType(CursorType cursorType)
{
Screen::setCursorType(cursorType);
[_view setCursorType: cursorType];
}