Prevent Opening new NSDocuments and show a warning message - cocoa

I have an NSDocument based app in which I want to limit the number of documents open at the same time (for a Lite version). I just want to have n documents, and if the user tries to open more than n, show a message with a link to the full app download.
I have managed to count the number of documents using NSDocumentController and, inside readFromFileWrapper, I can return FALSE. That prevents the new doc from opening, but it shows a standard error message. I don't know how to avoid that. I would like to open a new window from a nib.
Is there any way to prevent NSDocument showing the standard error message when returning FALSE from readFromFileWrapper? Or is there any other way to prevent the document from opening before readFromFileWrapper is called?

Try the init method, which is called both when creating a new document and when opening a saved document. You simply return nil if the limit has been reached. However, I have not tried this, and it might cause the same error to be displayed.
- (id)init {
if([[NSDocumentController documents] count] >= DOCUMENT_LIMIT) {
[self release];
return nil;
}
...
}
In case the same error is displayed, you could use a custom NSDocumentController. Your implementations would check the number of open documents, display the message at the limit, and call the normal implementation otherwise.
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError {
if([[self documents] count] >= DOCUMENT_LIMIT) {
// fill outError
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
- (id)openDocumentWithContentsOfURL:(NSURL *)absoluteURL display:(BOOL)displayDocument error:(NSError **)outError {
NSDocument *doc = [self documentForURL:absoluteURL];
if(doc) { // already open, just show it
[doc showWindows];
return doc;
}
if([[self documents] count] >= DOCUMENT_LIMIT) {
// fill outError
return nil;
}
return [super openDocumentWithContentsOfURL:absoluteURL display:displayDocument];
}

Related

Validating URL from drag and drop in a sandbox

With file access in a sandboxed osx app with swift in mind, does it work the same with URLs provided via Finder or other apps drops?
As there's no NSOpenPanel call to afford folder access as in this example, just urls - I think the folder access is implicit since the user dragged the file from the source / desktop "folder" much the same as implicit selection via the open dialog.
I have not begun the sandbox migration yet but wanted to verify my thinking was accurate, but here's a candidate routine that does not work in sandbox mode:
func performDragOperation(_ sender: NSDraggingInfo!) -> Bool {
let pboard = sender.draggingPasteboard()
let items = pboard.pasteboardItems
if (pboard.types?.contains(NSURLPboardType))! {
for item in items! {
if let urlString = item.string(forType: kUTTypeURL as String) {
self.webViewController.loadURL(text: urlString)
}
else
if let urlString = item.string(forType: kUTTypeFileURL as String/*"public.file-url"*/) {
let fileURL = NSURL.init(string: urlString)?.filePathURL
self.webViewController.loadURL(url: fileURL!)
}
else
{
Swift.print("items has \(item.types)")
}
}
}
else
if (pboard.types?.contains(NSPasteboardURLReadingFileURLsOnlyKey))! {
Swift.print("we have NSPasteboardURLReadingFileURLsOnlyKey")
}
return true
}
as no URL is acted upon or error thrown.
Yes, the file access is implicit. As the sandbox implementation is poorly documented and had/has many bugs, you want to work around URL and Filenames. The view should register itself for both types at initialisation. Code is in Objective-C, but API should be the same.
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil]];
Then on performDragOperation:
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
BOOL dragPerformed = NO;
NSPasteboard *paste = [sender draggingPasteboard];
NSArray *typesWeRead = [NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil];
//a list of types that we can accept
NSString *typeInPasteboard = [paste availableTypeFromArray:typesWeRead];
if ([typeInPasteboard isEqualToString:NSFilenamesPboardType]) {
NSArray *fileArray = [paste propertyListForType:#"NSFilenamesPboardType"];
//be careful since this method returns id.
//We just happen to know that it will be an array. and it contains strings.
NSMutableArray *urlArray = [NSMutableArray arrayWithCapacity:[fileArray count]];
for (NSString *path in fileArray) {
[urlArray addObject:[NSURL fileURLWithPath:path]];
}
dragPerformed = //.... do your stuff with the files;
} else if ([typeInPasteboard isEqualToString:NSURLPboardType]) {
NSURL *droppedURL = [NSURL URLFromPasteboard:paste];
if ([droppedURL isFileURL]) {
dragPerformed = //.... do your stuff with the files;
}
}
return dragPerformed;
}

Why is CoreLocation (Mac OS X) not remembering my choice to allow its use?

I'm probably being very stupid, but I can't work out why the following is happening:
I have an app on Mac OS X which uses CoreLocation. Below is the the relevant code:
It asks for permission to use my location every time it launches and never remembers that I have already granted use of location data.
The app never appears in the 'Privacy' Preference pane.
Am I missing something?
Thanks.
#pragma mark - General Instance Methods
- (void)determineLocation
{
if (self.locationManager)
{
DDLogWarn(#"determinLocation called, but we already have a locationManager instance variable...");
DDLogWarn(#"This is a bug.");
}
self.currentlocationName = #"Location unknown";
if (![CLLocationManager locationServicesEnabled])
{
DDLogWarn(#"Location Services not enabled.");
[self fallBackToHardcodedLocation];
return;
}
// Location services are available.
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
// Coarse-grained location accuracy required.
self.locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;
self.locationManager.distanceFilter = 1000; // meters - 1km
[self.locationManager startUpdatingLocation];
}
#pragma mark - Location Manager Delegate Methods
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
DDLogVerbose(#"Our authorisation to use the location manager has changed.");
switch (status) {
case kCLAuthorizationStatusNotDetermined:
DDLogError(#"LocationManager Authorisation status not determined. (User hasn't chosen yet)");
break;
case kCLAuthorizationStatusAuthorized:
DDLogVerbose(#"We are now authorised for locationServices.");
[self.locationManager startUpdatingLocation];
break;
case kCLAuthorizationStatusDenied:
DDLogWarn(#"LocationManager: We are now explicitly denied!");
[self.locationManager stopUpdatingLocation];
break;
case kCLAuthorizationStatusRestricted:
DDLogWarn(#"LocationManager We are now restricted! (not authorised - perhaps due to parental controls...)");
[self.locationManager stopUpdatingLocation];
break;
default:
break;
}
}
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
// Main callback for location discovery..§
CLLocation* location = [locations lastObject];
NSDate* eventDate = location.timestamp;
NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];
if (abs(howRecent) < 15.0)
{
// Event is recent. Do something with it.
if (location.horizontalAccuracy > 1000)
{
DDLogWarn(#"Location Accuracy is worse than 1km, discarding...");
} else {
[self updateLocation:location];
}
return;
}
DDLogWarn(#"Stale location data...");
[self updateLocation:location];
}
It always asks permission: (kCLAuthorizationStatusNotDetermined appears to always be the case.)
2015-02-07 01:03:36:127 Central Heating[2260:30b] LocationManager Authorisation status not determined. (User hasn't chosen yet)
(Dialog text: "Central Heating" would like to use your current location. Your location is needed for weather forecasting. [Don't allow] [OK])
As Gordon Mentioned, this issue is resolved by signing the application.

Problems implementing the NSBrowserDelegate protocol

I'm able to get my NSBrowser instance to display the correct data in the first column. When I select one of the options, however, the next column simply displays the same set of options. I have read the docs, looked at all of Apple's relevant sample code, and just about everything I could find on the internet but I simply can't figure out the correct way to implement the required methods. The data I'm supplying to the browser is an array of dictionaries. Each dictionary in turn contains a "children" key that is another array of dictionaries. And those dictionaries have their own "children" key that are also arrays of dictionaries, etc. Using JSON for descriptive purposes (objects are dictionaries, arrays are arrays), it looks like this:
data = [
{
name: 'David',
children:[
{
name: 'Sarah',
children: {...}
},
{
name: 'Kevin',
children: {...}
}
]
},
{
name: 'Mary',
children:[
{
name: 'Greg',
children: {...}
},
{
name: 'Jane',
children: {...}
}
]
}
]
So the first column should show "David" and "Mary". If "David" is selected, the next column should show "Sarah" and "Kevin", and so on.
My current implementation relies on a custom method I created that is supposed to translate the browser's index path into the corresponding NSArray level from the provided data. This method looks like:
- (NSArray *)getSelectionInBrowser:(NSBrowser *)browser
{
NSArray *selection = browserData;
NSIndexPath *path = [browser selectionIndexPath];
for (NSUInteger i = 0; i < path.length; i++) {
selection = [[selection objectAtIndex:i] objectForKey:#"children"];
}
return selection;
}
My implementation of the required NSBrowserDelegate protocol methods looks like:
- (NSInteger)browser:(NSBrowser *)sender numberOfRowsInColumn:(NSInteger)column
{
return [[self getSelectionInBrowser:sender] count];
}
- (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
return [[self getSelectionInBrowser:browser] count];
}
- (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
return [self getSelectionInBrowser:browser];
}
- (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
return ![item isKindOfClass:[NSMutableArray class]];
}
- (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
return nil;
}
- (void)browser:(NSBrowser *)browser willDisplayCell:(NSBrowserCell *)cell atRow:(NSInteger)row column:(NSInteger)column {
NSArray *selection = [self getSelectionInBrowser:browser];
cell.title = [[selection objectAtIndex:row] objectForKey:#"name"];
}
The first column of the NSBrowser is populated with the correct names. However, as soon as I make a selection the program crashes with the error -[__NSArrayM objectAtIndex:]: index 4 beyond bounds [0 .. 0]. After doing some debugging, the line of code it crashes on is the objectAtIndex: call in my custom getSelectionInBrowser:.
That doesn't fully surprise me because even before the crash I figured I was doing something wrong by relying on that custom method to retrieve the current selection. I imagine this work should be done within the delegate methods themselves and, when implemented correctly, the current selection should be accessible in the item variable that is provided in many of those methods. However, I couldn't get that to work. The item variable always seemed to be simply the root data object rather than reflecting the most "drilled-down" selection.
So how do I correct my implementation?
Solved it! Here is my final working code. No need for that custom getSelection... method, and a couple of the delegate methods I had were unnecessary (only used of you are NOT going with the "item-based API").
- (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
if (item) {
return [[item objectForKey:#"children"] count];
}
return [browserData count];
}
- (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
if (item) {
return [[item objectForKey:#"children"] objectAtIndex:index];
}
return [browserData objectAtIndex:index];
}
- (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
return [item objectForKey:#"children"] == nil;
}
- (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
return [item objectForKey:#"name"];
}
The first method is how you tell the NSBrowser the number of rows there should be. The second method is where you determine what data should be represented in a given row (index). In both cases, you must first check to see if item actually exists. If it doesn't, that's because you are at the root of the data (first column in the NSBrowser). Only when a row (or item!) in the NSBrowser gets selected will the item variable hold anything. The final method should return the string you wish to show in the given row.
Hopefully this helps people in the future.

Wrong callback in Objective-C when calling SOAP service

I have 2 SOAP services that I want to call from an IPad app.
One is used to Log the user in (SecurityASMX), the other is one that returns the current username (SecuredCalls) once logged in.
I can call the SecurityASMX no problem using the following code. The Async call callback is operation :
- (IBAction) OnButtonClick:(id) sender {
bindingSecurity = [[SecurityASMXSvc SecurityASMXSoapBinding] initWithAddress:#"http://myserver/Azur.IPADTest.Web.Services/public/Security.asmx"];
bindingSecurity.logXMLInOut = YES;
SecurityASMXSvc_Login *requestLogin = [[SecurityASMXSvc_Login alloc] init];
requestLogin.strUsername = #"test";
requestLogin.strPassword = #"testpass";
[bindingSecurity LoginAsyncUsingParameters:requestLogin delegate:self];
[requestLogin release];
self.label.text = #"Login in progress";
}
- (void) operation:(SecurityASMXSoapBindingOperation *)operation completedWithResponse:(SecurityASMXSoapBindingResponse *)response
{
[NSThread sleepForTimeInterval:2.0];
self.label.text = #"Login Done!";
}
This works fine.
However, in the same code file, I have a binding to my second web service to return the username with the following code. The async call callback is operationSecure :
- (IBAction) OnButtonSecureCallClick:(id) sender {
bindingSecuredCalls = [[SecureCallsSvc SecureCallsSoapBinding] initWithAddress:#"http://myserver/Azur.IPADTest.Web.Services/private/SecureCalls.asmx"];
bindingSecuredCalls.logXMLInOut = YES;
SecureCallsSvc_ReturnUserName *requestReturnUserName = [[SecureCallsSvc_ReturnUserName alloc] init];
[bindingSecuredCalls ReturnUserNameAsyncUsingParameters:requestReturnUserName delegate:self];
[requestReturnUserName release];
self.label.text = #"Get UserName In Progress";
}
- (void) operationSecure:(SecureCallsSoapBindingOperation *)operation completedWithResponse:(SecureCallsSoapBindingResponse *)response
{
[NSThread sleepForTimeInterval:2.0];
self.label.text = #"Get Username Done!";
}
The problem is that when the call to ReturnUserName returns, the method that gets called is the one for the login (operation) and not the one I want (operationSecure).
How can I tell my second webservice binding to call the second callback?
Thanks!
First thing would be to check if the API you're using (I assume it's a third party API) allows you to specify the callback method.
If not, you can work with the operation parameter and use isKindOfClass to see what is actually being passed.
- (void) operation:(SecurityASMXSoapBindingOperation *)operation completedWithResponse:(SecurityASMXSoapBindingResponse *)response
{
[NSThread sleepForTimeInterval:2.0];
if([operation isKindOfClass:[SecurityASMXSoapBindingOperation class]])
{
self.label.text = #"Login Done!";
}
else if([operation isKindOfClass:[SecureCallsSoapBindingOperation class]])
{
self.label.text = #"Get Username Done!";
}
}
Ideally you'd set the type of operation and response parameters to be the superclass of the respective objects returned.

NSTableView with multiple columns

What is an easy way to set up my NSTableView with multiple columns to only display certain data in one column. I have the IBOutlets set up, but I don't know where to go from there.
Assuming you're not using Cocoa Bindings/Core Data, you can display data in an NSTableView by implementing two methods from the NSTableViewDataSource protocol. Typically your controller will implement the protocol, so open the controller .m file and add these methods to the controller's #implementation:
- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView {
return 25; // fill this out
}
– (id) tableView:(NSTableView*)tableView
objectValueForTableColumn:(NSTableColumn*)column
row:(int)row {
return row % 3 ? #"Tick..." : #"BOOM!"; // fill this out
}
You need to set the table's dataSource property to the controller. In Interface Builder control-drag from the table view to the controller and set dataSource. Now build and run and you should see your data in the table.
If you only want to fill out one column, add an IBOutlet NSTableColumn* to your controller; let's call it explosiveColumn. In Interface Builder, control-drag from the controller to the column you want to fill in and set explosiveColumn. Then, in tableView:objectValueForTableColumn:row: you can test if the column parameter is the same object as the one that the outlet is set to:
– (id) tableView:(NSTableView*)tableView
objectValueForTableColumn:(NSTableColumn*)column
row:(int)row {
if (column == explosiveColumn) {
return row % 3 ? #"Tick..." : #"BOOM!";
} else {
// other columns blank for now
return nil;
}
}
This tutorial might be useful: http://www.cocoadev.com/index.pl?NSTableViewTutorial
Here's an example using multiple table views with data source methods and a document based application:
#pragma mark - Data Source Methods
- (NSInteger) numberOfRowsInTableView:(NSTableView *)tv
{
if (tv == racerTableView)
return [racerList count];
else if (tv == vehicleTableView)
return [vehicleList count];
else
return 0; // something wrong here...
}
- (id)tableView:(NSTableView *)tv objectValueForTableColumn:(NSTableColumn *)col
row:(NSInteger)rowi
{
NSString *colid = [col identifier];
if (tv == racerTableView){
NHRacers *racer = [racerList objectAtIndex:rowi];
return [racer valueForKey:colid];
}
else if (tv == vehicleTableView){
NHVehicles *vehicle = [vehicleList objectAtIndex:rowi];
return [vehicle valueForKey:colid];
}
else
return 0; // something wrong here...
}
- (void)tableView:(NSTableView *)tv setObjectValue:(id)obj forTableColumn:(NSTableColumn *)col row:(NSInteger)rowi
{
NSString *colid = [col identifier];
if (tv == racerTableView) {
NHRacers *racer = [racerList objectAtIndex:rowi];
[racer setValue:obj forKey:colid];
}
else if (tv == vehicleTableView){
NHVehicles *vehicle = [vehicleList objectAtIndex:rowi];
[vehicle setValue:obj forKey:colid];
}
else
nil; // something wrong here...
[self updateChangeCount:NSChangeDone];
}
The tableview datasource outlets are set to the File's Owner and the File's Owner has set vehicleTableView and racerTableView to their respective "Table View" in the IB. The colid key checks the identifier (set in IB by selecting the table view column under the "Identity" drop down, while the "Identity Inspector" is shown). These values were chosen to be the KVC (key coding compliant) properties of the classes being displayed in the table views: use lower case first letter (see apple docs for rest).
For example:
(in NHVehicles.h)
#interface NHVehicles : NSObject
{
NSUInteger entry;
NSString *name;
NSString *vehicleClass;
}
#property NSUInteger entry;
#property NSString *name, *vehicleClass;
#end
(in NHVehicles.m)
#implementation NHVehicles
#synthesize entry, name, vehicleClass;
#end
for this tableView, "entry", "name" and "vehicleClass" would be typed (w/o ") into the identifier fields for their respective columns.
If you don't want to show some data in the class, simply do not enter the key for the column identifier. A word of caution: I am using Xcode 4.5.1 and I noticed that once I had entered a few keys for a particular column identifiers and then changed my mind about and attempted to clear the text, it complained when I deleted the text from the identifier field (I could no longer leave the field blank for the columns that I had edited). This was not difficult to work around, but it was a surprise.

Resources