NSTextView scrolling behavior and selecting behavior - objective-c

I have an NSTextView that I'm outputting text from NSTask. Everything works as expected except the scrolling and selecting behaviors.
1: If I try to scroll up, the position of my scroll snaps back to the bottom instantly after I let go. Any ideas? I've looked through quite a bit of documentation about this and can't find anything about it.
2: If I select text, it removes it. I just want it to select so I can copy and paste. Lost on this one too.
Any tips or pointers would be most welcome. Thanks.
- (id)init
{
[super init];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(readPipe:)
name:NSFileHandleReadCompletionNotification
object:nil];
return self;
}
- (void)kicked
{
task = [[NSTask alloc] init];
[task setLaunchPath:[self.kickLocationTextField stringValue]];
[task setArguments:kickBuild];
NSPipe *pipe = [[NSPipe alloc] init];
fileHandle = [pipe fileHandleForReading];
[fileHandle readInBackgroundAndNotify];
[task setStandardOutput:pipe];
[task setStandardError:pipe];
[task launch];
[task release];
[pipe release];
}
- (void)readPipe:(NSNotification *)notification
{
NSData *data;
NSString *text;
if( [notification object] != fileHandle )
{
return;
}
data = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
text = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
[nsTaskOutput insertText:text];
[text release];
if (data != 0)
{
[fileHandle readInBackgroundAndNotify];
}
}

Try this instead of insertText::
NSScroller *scroller = nTaskOutput.enclosingScrollView.verticalScroller;
BOOL shouldScrollToBottom = scroller.doubleValue == 1.0;
NSTextStorage *ts = nTaskOutput.textStorage;
[ts replaceCharactersInRange:NSMakeRange(ts.length, 0) withString:text];
if (shouldScrollToBottom) {
NSRect bounds = nTaskOutput.bounds;
[nTaskOutput scrollPoint:NSMakePoint(NSMaxX(bounds), NSMaxY(bounds))];
}

Related

Objective-C: How to keep an application in dock?

I am trying to keep my application in dock, however I am not able to do this.
How I am trying to do?
#interface UserDefaultsHelper : NSUserDefaults
- (BOOL)addApplicationToDock:(NSString *)path;
- (BOOL)removeApplicationFromDock:(NSString *)name;
#end
#implementation UserDefaultsHelper
- (BOOL)addApplicationToDock:(NSString *)path {
NSDictionary *domain = [self persistentDomainForName:#"com.apple.dock"];
NSArray *apps = [domain objectForKey:#"persistent-apps"];
NSArray *matchingApps = [apps filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"%K CONTAINS %#", #"tile-data.file-data._CFURLString", path]];
if ([matchingApps count] == 0) {
NSMutableDictionary *newDomain = [domain mutableCopy];
NSMutableArray *newApps = [apps mutableCopy];
NSDictionary *app = [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObjectsAndKeys:path, #"_CFURLString", [NSNumber numberWithInt:0], #"_CFURLStringType", nil] forKey:#"file-data"] forKey:#"tile-data"];
[newApps addObject:app];
[newDomain setObject:newApps forKey:#"persistent-apps"];
[self setPersistentDomain:newDomain forName:#"com.apple.dock"];
return [self synchronize];
}
return NO;
}
- (BOOL)removeApplicationFromDock:(NSString *)name {
NSDictionary *domain = [self persistentDomainForName:#"com.apple.dock"];
NSArray *apps = [domain objectForKey:#"persistent-apps"];
NSArray *newApps = [apps filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"not %K CONTAINS %#", #"tile-data.file-data._CFURLString", name]];
if (![apps isEqualToArray:newApps]) {
NSMutableDictionary *newDomain = [domain mutableCopy];
[newDomain setObject:newApps forKey:#"persistent-apps"];
[self setPersistentDomain:newDomain forName:#"com.apple.dock"];
return [self synchronize];
}
return NO;
}
#end
I found the above code from here. Code Source
the above code is getting compiled and running, however it's adding the application in the application's dock.
I am a newbie here. Please be lenient towards me as I do not have OS level of knowledge.
Many thanks in advance for your help and attention.
The code provided in question is correct the only problem was that the dock was not getting refreshed. So we just need to refresh the DOCK
In order to refresh dock I am calling a terminal command from obj-c
Terminal Command killall dock
....code...
[self runCommand:#"killall Dock"];
}
-(NSString*)runCommand:(NSString*)commandToRun;
{
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/sh"];
NSArray *arguments = [NSArray arrayWithObjects:
#"-c" ,
[NSString stringWithFormat:#"%#", commandToRun],
nil];
NSLog(#"run command: %#",commandToRun);
[task setArguments: arguments];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
NSString *output;
output = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
return output;
}

NSTask data split by newline \n

I'm new to objective-C, so please forgive me if I'm missing something. But we all have to start somewhere :)
I have a snippet of code I got from another open source project that executes a command and passes the result to another method. What I need to do is listen for each new line printed to stdout and do something with each line.
The code snippet I'm working with is the following:
NSMutableArray *args = [NSMutableArray array];
NSString *input = [details valueForKey:#"input"];
for (NSString *i in [input componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]) {
[args addObject:i];
}
NSTask *scriptTask = [NSTask new];
NSPipe *outputPipe = [NSPipe pipe];
if ([_NSFileManager() isExecutableFileAtPath:scriptPath] == NO) {
NSArray *chmodArguments = #[#"+x", scriptPath];
NSTask *chmod = [NSTask launchedTaskWithLaunchPath:#"/bin/chmod" arguments:chmodArguments];
[chmod waitUntilExit];
}
[scriptTask setStandardOutput:outputPipe];
[scriptTask setLaunchPath:scriptPath];
[scriptTask setArguments:args];
NSFileHandle *filehandle = [outputPipe fileHandleForReading];
[scriptTask launch];
[scriptTask waitUntilExit];
NSData *outputData = [filehandle readDataToEndOfFile];
NSString *outputString = [NSString stringWithData:outputData encoding:NSUTF8StringEncoding];
if (NSObjectIsNotEmpty(outputString)) {
[self.world.iomt inputText:outputString command:IRCPrivateCommandIndex("privmsg")];
}
So, rather than waiting for the process to complete then doing something with the result, I need to wait for each new line that gets printed by the command to stdout.
My background is mostly in web dev, so I guess if you were using Node.js and event emitters my aim would look similar to the following:
task = new Task("ls");
task.addListener("newline", function(data) {
somethingElse("sendmsg", data);
});
task.run();
Hopefully you understand what I'm tying to achieve. Thanks!
What you can do is add an observer to the NSFileHandle to notify you when it reads something.
example:
[fileHandler readInBackgroundAndNotify];
//Need to set NSTask output pipes
[self.task setStandardOutput: outputPipe];
[self.task setStandardError: outputPipe];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(readFromTask:) name:NSFileHandleReadCompletionNotification object:fileHandler];
-(void)readFromTask:(NSNotification *)aNotifcation;
{
NSData *data = [[aNotifcation userInfo] objectForKey: NSFileHandleNotificationDataItem];
if (data != 0) {
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// Do something with the text
[text release];
[[aNotifcation object] readInBackgroundAndNotify];
}
}
Then there's a notififcation you can add to the task to indicate when its completed so you can do clean up.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(taskEnded:) name:NSTaskDidTerminateNotification object:task];
- (void)taskEnded:(NSNotification *) aNotifcation
{
NSData *data = [[aNotifcation userInfo] objectForKey: NSFileHandleNotificationDataItem];
if (data != 0) {
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[text release];
}
[self.task release];
}
So from your code, you could remove:
[scriptTask waitUntilExit];
And move the data handling into the notifications.
NSData *outputData = [filehandle readDataToEndOfFile];
NSString *outputString = [NSString stringWithData:outputData encoding:NSUTF8StringEncoding];
if (NSObjectIsNotEmpty(outputString)) {
[self.world.iomt inputText:outputString command:IRCPrivateCommandIndex("privmsg")];
}
That's a rough idea, there's a good post at
http://www.cocoabuilder.com/archive/cocoa/306145-nstask-oddity-with-getting-stdout.html

Can someone help me spot a leak in this NSPipe/NSFileHandle code?

So I'm having this issue where once this NSFileHandle/NSPipe gets triggered... my CPU use and memory start going crazy. Problem is I'm finding it hard to find this leak. Any advice or help is appreciated. Cheers.
.h
NSTask *task;
NSPipe *pipe;
NSFileHandle *fileHandle;
#property (weak) IBOutlet NSTextField *commandInputTextField;
#property (unsafe_unretained) IBOutlet NSTextView *nsTastOutput;
#property (weak) IBOutlet NSButton *useOutputSwitch;
.m
- (id)init
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(readPipe:)
name:NSFileHandleReadCompletionNotification
object:nil];
}
- (void)tasker
{
task = [[NSTask alloc] init];
[task setLaunchPath:#"/bin/bash"];
NSArray *argumentBuilder = [[_commandInputTextField stringValue] componentsSeparatedByString:#" "];
[task setArguments:argumentBuilder];
// Pipe output to ScrollView
if (_useOutputSwitch.state == YES)
{
if (!pipe)
{
pipe = [[NSPipe alloc] init];
}
fileHandle = [pipe fileHandleForReading];
[fileHandle readInBackgroundAndNotify];
[task setStandardOutput:pipe];
[task setStandardError:pipe];
}
// Launch task
[task launch];
}
- (void)readPipe:(NSNotification *)notification
{
NSData *data;
NSString *text;
if( [notification object] != fileHandle )
{
return;
}
data = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
text = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
NSScroller *scroller = [[_nsTastOutput enclosingScrollView] verticalScroller];
BOOL shouldScrollToBottom = [scroller doubleValue] == 1.0;
NSTextStorage *ts = [_nsTastOutput textStorage];
[ts replaceCharactersInRange:NSMakeRange([ts length], 0) withString:text];
if (shouldScrollToBottom)
{
NSRect bounds = [_nsTastOutput bounds];
[_nsTastOutput scrollPoint:NSMakePoint(NSMaxX(bounds), NSMaxY(bounds))];
}
if (data != 0)
{
[fileHandle readInBackgroundAndNotify];
}
}
I met a similar problem while using the readabilityHandler. I finally find out closing the fileHandle after task complete resolves the problem. Wish it helps your case.

NSTask only returning standardError in release build

First of all, when debugging and running in Xcode everything works as expected.
But when I try to "share" my app, i.e. make a release build, my NSTask won't output any standardOutput while standardErrors ARE put out. How is that possible?
My code
- (id)initWithWindow:(NSWindow *)window {
self = [super initWithWindow:window];
if (self) {
// Initialization code here.
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(readPipe:) name:NSFileHandleReadCompletionNotification object:nil];
return self;
}
-(void) watchFile:(NSNotification *)notification {
NSString *path = [[notification userInfo] valueForKey:#"path"];
task = [[NSTask alloc] init];
[task setLaunchPath:#"/usr/bin/compass"];
[task setCurrentDirectoryPath:path];
NSArray *arguments;
arguments = [NSArray arrayWithObjects: #"watch",#"--boring", nil];
[task setArguments: arguments];
NSPipe *outPipe, *errPipe;
outPipe = [NSPipe pipe];
errPipe = [NSPipe pipe];
[task setStandardOutput: outPipe];
[task setStandardError: errPipe];
[task setStandardInput: [NSPipe pipe]];
standardHandle = [outPipe fileHandleForReading];
[standardHandle readInBackgroundAndNotify];
errorHandle = [errPipe fileHandleForReading];
[errorHandle readInBackgroundAndNotify];
[self setSplitterPosition:0.0f];
[task launch];
}
-(void)readPipe:(NSNotification *)notification {
NSLog(#"reading pipe");
NSData *data;
NSString *text;
if(!([notification object] == standardHandle) && !([notification object] == errorHandle)) {
return;
}
data = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
text = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
if ([data length] == 0) {
//error
[self setSplitterPosition:150.0f];
return;
}
[terminalViewController updateTerminal:text];
if(![text isEqualToString:#"\n"]) [self growlAlert:text title:#"Compapp"];
[text release];
if(task) [[notification object] readInBackgroundAndNotify];
}
/usr/bin/compass is not a binary installed in the standard installation of OSX (I don't have any binary named compass in my /usr/bin on my Mac)
So it seems quite logical that when your app is running on another Mac -- that does not have /usr/bin/compass installed -- and you try to run this task, it can't find it and only output some error on stderr.
If you mean after release you are trying to debug, you need to create a file called Entitlements.plist and set Can be debugged before you build and archive.
if this is still an open issue for you, then checkout the answer I posted at this link: How to use a determinate NSProgressIndicator to check on the progress of NSTask? - Cocoa
it provides a good template for creating an async NSTask and for reading from standard output or standard error.

NSTask blocking the main thread

I'm using NSTask, but when I launch the task it blocks the main thread (so I can't update it) until the task ends. This is my code:
NSString *hostsforping = #"google.es";
pingdata = [[NSTask alloc] init];
[pingdata setLaunchPath: #"/sbin/ping"];
NSArray *pingargs;
pingargs = [NSArray arrayWithObjects: #"-c 5", hostsforping, nil];
[pingdata setArguments: pingargs];
NSPipe *pingpipe;
pingpipe = [NSPipe pipe];
[pingdata setStandardOutput: pingpipe];
NSFileHandle *pingfile;
pingfile = [pingpipe fileHandleForReading];
[pingdata launch];
NSData *pingdata1;
pingdata1 = [pingfile readDataToEndOfFile];
NSString *pingstring;
pingstring = [[NSString alloc] initWithData: pingdata1 encoding: NSUTF8StringEncoding];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(taskDidTerminate:)
name:NSTaskDidTerminateNotification
object:nil];
}
- (void) taskDidTerminate:(NSNotification *)notification {
NSLog(#"end");
}
I've been reading that -waitUntilExit does block the main thread, but I'm not using it, so I don't know what I'm doing wrong.
Run the task on a background thread, the readDataToEndOfFile is blocking the main thread.
// Offload the method onto a background thread, could also use Grand Central Dispatch
[self performSelectorInBackground:#selector(startTask) withObject:nil];
- (void)startTask {
NSString *hostsforping = #"google.es";
NSTask *pingdata = [[NSTask alloc] init];
[pingdata setLaunchPath: #"/sbin/ping"];
NSArray *pingargs;
pingargs = [NSArray arrayWithObjects: #"-c 5", hostsforping, nil];
[pingdata setArguments: pingargs];
NSPipe *pingpipe;
pingpipe = [NSPipe pipe];
[pingdata setStandardOutput: pingpipe];
NSFileHandle *pingfile;
pingfile = [pingpipe fileHandleForReading];
[pingdata launch];
NSData *pingdata1;
pingdata1 = [pingfile readDataToEndOfFile];
NSString *pingstring;
pingstring = [[NSString alloc] initWithData: pingdata1 encoding: NSUTF8StringEncoding];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(taskDidTerminate:)
name:NSTaskDidTerminateNotification
object:nil];
}
- (void) taskDidTerminate:(NSNotification *)notification {
// Note this is called from the background thread, don't update the UI here
NSLog(#"end");
// Call updateUI method on main thread to update the user interface
[self performSelectorOnMainThread:#selector(updateUI) withObject:nil waitUntilDone:NO];
}
Since, you use the readDataToEndOfFile, you're implicit saying that your NSTask need a blocking execution. I could suggest you to wrap your code around GCD brackets, using dispatch_async. For example:
- (void)executeShellScript {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSString *execPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:#"myScript.sh"];
NSPipe *pipe = [NSPipe pipe];
NSFileHandle *file = pipe.fileHandleForReading;
NSTask *task = [[NSTask alloc] init];
task.launchPath = #"/bin/sh";
task.arguments = #[execPath];
task.standardOutput = pipe;
[task launch];
NSData *data = [file readDataToEndOfFile]; //readDataToEndOfFile is blocking the main thread.
[file closeFile];
NSString *bashOutput = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog (#"shell returned:\n%#", bashOutput);
});
}