Scrolling process status window - objective-c

As part of a Mac application I am working on, the user fills out a screen full of stuff and then presses a 'process' button. There are edits performed and if everything passes the edit, a couple of minute process is performed which either end ok or not. I would like to have that process spit out a series of status and processing messages into a separate scrolling window so that if something goes bad, the user can go back through the log and see if anything shows up there.
What would be the best objects and methods for me to review and use for this type of processing?
Added 11/24/2011
As per the first suggestion, I created a second XIB, created a NSWindowController to match and put it all together as some prep work. When the button in pressed in the app delegate, I have this thing do the following:
- (IBAction)runButtonPressed:(id)sender {
RunResultWindow *wc;
wc = [[RunResultWindow alloc] initWithWindowNibName:#"RunResultWindow"];
[wc showWindow:self];
}
RunResultWindow is the name of the XIB and the NSWindowController class that controls it. I also added a finish button and wired that up with the intention of having the results of the process fill up the text window and then hang there until the user presses 'done' or 'finish' or whatever I wind up calling the button.
It actually shows the window when I press the button on the main window but when the code for the button finishes, the window vanishes. Clearly I am leaving out (an important) step.
Once I get the window then I can add the text view etc.... and get that working. What I would like is for the new Window to get focus and then close out when the user presses the 'done' button.
Additionally, I got the window for the window controller from the window method (it returned an address) and tried a couple of window focus methods in the windowDidLoad method of the NSWindowController but no dice.
Thanks again for whatever info I can get on this.
Added 11/25/2011
Duh. Maybe if I make the class instance an ivar instead of embedding it in the button method it will work and, lo, it did. Le Oops.

Sounds like you want to drop a NSTextView into a window where one can select & scroll the text but not edit the contents.
You can insert text as easily as using the insertText: method.

So... The NSTextView and insertText combination worked out somewhat ok but I don't' think it is the final answer. First, my understanding is that insertText is really only meant for user input and not background 'system' input to a NXTextStorage object. I'm not sure why but that's fine so I'll avoid it. There are other options. I did find the beginEdit and endEdit methods and it works pretty much about the same way though I have some more work to do on some detail delegate methods.
The part that doesn't work so well is getting the NSScrollView in the window to update on demand. I do the beginEdit and endEdit stuff and am able to update the NSTextStorage object properly. I can do this multiple times in the same method (a test button on the window containing the scroll view). I can tell that because print-object in debug shows me what I'm expecting at the right times. However, I'd like to be able to show an updated NSScrollView multiple times during the court of the windowDidLoad method. The scroll view updates properly when the button push method ends.
Here is some sample code. I do mix insertText and the begin/end edit methods in here but it was more of a test thing than any code I would use for real....
(IBAction)FinishButtonPush:(id)sender {
NSString *teststring;
teststring = [NSString stringWithString: #"show"];
[RunResultWindowTextView setString:teststring];
teststring = [NSString stringWithString: #"show show"];
[RunResultWindowTextView setString:teststring];
teststring = [NSString stringWithString: #"show show show show"];
[RunResultWindowTextView setString:teststring];
[RunResultWindowTextView insertText:#"123"];
NSTextStorage *tempTextStorage;
tempTextStorage = [RunResultWindowTextView textStorage];
[tempTextStorage beginEditing];
[tempTextStorage replaceCharactersInRange:NSMakeRange(5,13)
withString:#"Hello to you!"];
[tempTextStorage endEditing];
[tempTextStorage beginEditing];
[tempTextStorage replaceCharactersInRange:NSMakeRange(10,13)
withString:#"second change"];
[tempTextStorage endEditing];
[RunResultWindowTextView insertText:#"xxx123"];
[RunResultWindowTextView insertText:#"xxx123567"];
}
Even though the NSTextStorage object is updated properly, the scroll view only updates when the method completes. My understanding is that processEdit is called automatically during endEdit. I added processEdit in there just to see and all I got was either abends or no change depending on where I put the command.
This got deleted and I'm not sure why. If you're going to gong the post, please let me know why you did so. Can't improve my post unless I have an idea what was wrong with it....

Related

Capture the space key in a window containing several NSTextFields

I have a large window with a number of NSTextField fields on it, and I would like to intercept the space bar even while any of the text fields have focus, so that pressing space at any time will cause a separate action.
Subclassing NSTextField and overriding -keyUp: allows me to detect the space key, but a blank is still inserted in the text field. -keyDown: does not fire at all.
For other keys like Return and the arrow keys, I could use the control:textView:doCommandBySelector: delegate method, but it does not fire with the space bar.
There's a lot of advice out there for NSTextView, but I have found none for NSTextField.
Have you experimented with adding an event monitor? You create them via the NSEvent class method addLocalMonitorForEventsMatchingMask(_:handler:)
and they give you first dibs on the events you specify. In your case you'd add a monitor for keyDown events, then in the associated block you decide what to do with them. At the end of the block you can return the event just as it arrived, in which case it will behave normally, you can swallow the event (return nil), or you can modify the event by creating a new event object and returning that instead. Here's an example:
// You can add monitors anywhere - I've chosen the AppDelegate for
// this simple example.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask
handler:^NSEvent * (NSEvent * theEvent) {
if (theEvent.keyCode == 49) {
NSLog(#"swallowing spacebar");
theEvent = nil;
}
return theEvent;
}];
}
Since the behavior you're trying to implement is at the level of the window (or perhaps a view that's an ancestor of all of the text fields), you should consider implementing -performKeyEquivalent: at that level instead of trying to capture the space key in each individual text field.
In your implementation, check the event's modifierFlags to make sure none of Control, Option, or Command are pressed (assuming you only want to handle unmodified space key presses) and if its charactersIgnoringModifiers equals #" ". If so, do what you want and then return YES to indicate you've handled the key event. Otherwise, return NO to allow it to be handled normally.
If you know how to do it for NSTextView, then you know how to do it for NSTextField, because NSTextField uses an NSTextView called the "field editor" for editing. You can get the window's default field editor like so:
NSTextView* fieldEd = [myWindow fieldEditor: YES forObject: nil];
You can also use a window delegate to provide a custom field editor.
So you might want to look into ReactiveCocoa
Go to the video on this page and look at 5:14 to see something similar to what you might like.
ReactiveCocoa can be hooked into all your text fields and then any time a space is hit a signal handler could pick it up. (Probably, however, more than you want to get into).

Drag out to delete item

You know that effect when you drag out an item from the dock and that cloud drag cursor appears and when you let go it disappears with a poof effect? Similarly, in Xcode when you drag a breakpoint outside the line number gutter the same happens.
I would like to implement the same effect in my application but can't find the right way.
I have an NSImageView descendant to implement the NSDraggingSource and NSDraggingDestination protocols. I have several instances of this view which allow to drag their content between the others (a copy operation takes place in this scenario, but that's only relevant to show I have drag'n drop implmented and fully working for standard tasks).
Now, when I drag out an image from its view to anywhere (except another view instance) I want to have the delete operation taking place on drop. However the drag operation is fully controlled by the target view. I could manage to make them respond the way I want (even though this would be a lot of work), but it fails completely if I'm dragging outside my application.
If I could get the delete drag operation I could handle this however easily by:
- (void)draggedImage: (NSImage *)image
endedAt: (NSPoint)screenPoint
operation: (NSDragOperation)operation
{
if (operation == NSDragOperationDelete) {
NSRect rect = [self.window convertRectToScreen: [self convertRect: self.frame fromView: nil]];
NSShowAnimationEffect(NSAnimationEffectPoof, rect.origin, self.bounds.size, nil, nil, NULL);
}
}
I tried already to set the delete cursor like this:
- (void)draggingSession: (NSDraggingSession *)session
movedToPoint: (NSPoint)screenPoint
{
if (!NSPointInRect(screenPoint, self.window.frame)) {
[[NSCursor disappearingItemCursor] set];
}
}
(for simplicity this is for the entire windw at the moment). This works as long as I don't hit the desktop or a finder window. In starts flickering, probably because the Finder concurrently sets its own drag cursor. It is completely without effect when I hit the dock. This also happens when I define my own pasteboard data type.
Additionally, any other drop enabled view in my application will still accept my drag data (e.g. NSTextView) which I don't want to happen (I'm writing an NSURL to the dragging pasteboard with a custom scheme).
Update:
I've come a few steps further. As Peter already indicated it is essential to handle draggingSession:sourceOperationmaskForDraggingContext: which looks so in my code:
- (NSDragOperation) draggingSession: (NSDraggingSession *)session
sourceOperationMaskForDraggingContext: (NSDraggingContext)context;
{
switch(context) {
case NSDraggingContextOutsideApplication:
return NSDragOperationDelete;
break;
case NSDraggingContextWithinApplication:
default:
return NSDragOperationDelete | NSDragOperationMove;
break;
}
}
This solves 2 problems: 1) outside the application the drag operation is not accepted at all, 2) it keeps all standard views from accepting this operation too (because NSOutlineView, NSTextView etc. don't handle the given drag operations). Additionally, I created an own pasteboard datatype, but this doesn't seem to be necessary. Still it's clearer to have an own one.
Unfortunately, dropping outside of my NSImageView descendant (both within and outside the application) does not give me NSDragOperationDelete in draggedImage:endedAt:operation: (what I specified above) but NSDragOperationNone. Additionally the drag cursor when moving the mouse outside the application is the not-allowed one, not the disappearing-item. So, if someone could solve these two things I'd accept it as answer to my question.
There may be a less hacky way to do this, but I can think of one possibility: once the drag begins, create a transparent, borderless window the size of the desktop to be a dummy drag destination. You may need to call -setIgnoresMouseEvents: with NO to allow it to receive the drop even though it's transparent. You'll also have to set its window level above the menu bar (NSMainMenuWindowLevel + 1) to make sure that drags to the menu bar or Dock are still intercepted by your window.
As a drag destination, this window will have to check if one of your image views is under the cursor. You can use +[NSWindow windowNumberAtPoint:belowWindowWithWindowNumber:] to find the window below your transparent overlay window which is under the cursor. Then use -[NSApplication windowWithWindowNumber:] to determine if it's one of your app's windows and, if so, call -[NSView hitTest:] on its content view (converting the cursor coordinates as appropriate) to find the view. You can then forward NSDraggingDestination methods to that view as desired.
My guess is that NSDragOperationDelete only concerns drag/drops targeting the dock's Trash, and nothing else.
NSDragOperationGeneric should be a better fit.
Be sure not to mix methods, if you're going the 10.7 route, prefer :
-(void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
I just finished implementing something very similar to what you've described. Your code is quite similar to mine with a few exceptions:
Where you use draggedImage:endedAt:operation: I'm using draggingSession:session:endedAt:operation:. Also, in this method, I do not check the operation as all of my operations are set to generic. This is also where I perform the actual delete, so I only show the poof animation if the delete is successful.
In your draggingSession:session:movedToPoint:, you may also want to set the session's animatesToStartingPositionsOnCancelOrFail to false when the point is outside the window (this is also when you set the disappearingItemCursor) and set to true otherwise. This adds a final touch that once the deletion operation is completed, the dragged image doesn't rebound back to it's originating source location.
As for why you are not seeing the proper cursors, my guess is that you are using a Pasteboard type that other things (Finder, etc.) are willing to accept. I know you said you created your own Pastboard datatypes, but I would double-check that they are being used. When I made my own types, I gained control over the cursor. At least, I've not found anything that contends for my custom type.
OS X 10.7 or better:
- (void) draggingEnded: (id<NSDraggingInfo>) aInfo {
/*! Delete the current leaf if it is no longer represented in the subviews. */
RETURN_IF ( , NSNotFound != [self.visibleLeafs indexOfObject: iSelectedControl.representedObject] );
iIgnoreLeafsObservation = YES;
[self.leafs removeObject: iSelectedControl.representedObject];
iIgnoreLeafsObservation = NO;
}
This assumes that you have visually removed the object already. You could also set a flag during draggingEntered: and draggingExited: to indicate if drag session was last inside or outside of self.
In this program, leafs is the actual observed collection while visibleLeafs is a simulated collection of the representedObject of each visible control. Other NSDraggingDestination overloads present what will happen visually before it actually happens.
You can use the following command to make the poof appears at the mouse location:
NSShowAnimationEffect(NSAnimationEffectPoof, [NSEvent mouseLocation], NSZeroSize, NULL, NULL, NULL);
See the Application Kit Functions Reference for further information.

Why do my interface objects respond out of order?

I have an IBAction for when a button is clicked:
- (IBAction)importButtonClicked:(id)sender
And I want a series of events to take place like:
[_progressLabel becomeFirstResponder]; // I tried this but to no effect
_progressLabel.stringValue = BEGIN_IMPORT_STRING;
[_importButton setEnabled:FALSE];
_fileField.stringValue = #"";
[_progressIndicator startAnimation:nil];
But what ends up happening is the _progressIndicator animation takes place before the _progressLabel text appears. And often times the text won't appear untili the _progressIndicator animation has stopped. How do I fix that?
Put the work you're doing which takes time (I assume that's what the progress indicator is for) on a separate thread. You don't have to do this manually in Cocoa, but instead, use Grand Central Dispatch (GCD), NSOperationQueue or such a construct available. You'll find lots of resources on GCD.

Cannot set an NSWindow's position

I have a class that extends NSWindowController and I am trying to position the window it controls. The window displays all of the expected contents and functions correctly, but when I try and position its starting location on the screen in the initWithWindowNibName method, the position does not change. Here is the code:
NSPoint p = NSMakePoint(100, 50);
[[self window] setFrameTopLeftPoint:p];
This seems very straight forward and I'm not sure what the problem is.
Thanks for any ideas.
(Found the problem. I did not have the window wired up to the Class in IB.)
Wevah has the right idea, though I'll try to expand on it a bit.
If you were to try adding this line to your initWithWindowNibName: method:
NSLog(#"window == %#", [self window]);
You would likely see the following output to console:
window == (null)
In other words, the window is still nil, as init* methods are so early on in an object's lifetime that many IBOutlets or user interface items aren't quite "hooked up" yet.
Sending a message to nil is perfectly fine: it's simply ignored. So, basically your attempt to position the window has no effect because it basically equates to [nil doSomething];
The key then is to perform the positioning of the window later on in the controller object's lifetime, where the IBOutlets and other user interface objects are properly hooked up. As Wevah alluded to, one such method where things are properly hooked up is
- (void)awakeFromNib;
or in the case of NSWindowController, the following one as well:
- (void)windowDidLoad;
Hope this helps...
Try putting that code in awakeFromNib.

Event path in Mozilla plugin on Mac

I'm writing a mozilla plugin on the mac. I'm trying to capture events like button clicks, etc. I've done the normal thing of creating my views in inteface builder, and linking the sentActions to methods in my program. This works in stand-alone programs.
However, in my NPAPI plugin, those methods never get called. The button reacts, depresses, whatever, but it doesn't do its action.
Instead, the NPP_HandleEvent method gets called, but I never get the MouseDown or MouseUp event, only the UpdateEvt.
I set up the buttons to accept clicks via: (superview is the Mozilla view, topview is the top of my view hierarchy.)
[superView setNextResponder: topView];
[topView setNextResponder: nil];
[browserWindow makeFirstResponder: topView];
NEVER MIND: I'm an idiot. It IS calling the button sent actions. I was looking at the wrong method. That'll teach me to leave around a zoom: method when I'm actually using a doZoom: method... D'oh,.
So, the problem was that I wasn't able to get buttons to work. The buttons were supposed to (for example) zoom an image in an IKImageView. (or rather, zoom the view). It didn't appear that it was working. The screen was flashing a lot, but nothing was happening... I put a printf in my zoom method, and it was NEVER GETTING CALLED! and so I asked the question.
Later, I noticed that I wasn't TRYING to call zoom, I was calling doZoom! doZoom WAS being called. And the reason that it wasn't zooming was an unrelated problem.
The problem ended up being that I was sending setImage to my IKImageView on every event, which re-set the view to 1-1, rightside up mode. Once I took out the extra setImage call, things started to work.
In the unlikely event that anyone else every experiences this, the answer is my cunning plan for world domination:
step 1: Don't be an idiot.
step 2: ???????
step 3: Dominate the world.
(If I could master step 1, I might just be able to figure out what step 2 was B-)