Mac App Store, Sandbox and Open Recent...!


#1

Hi Jules!

Bit of an issue here!

There is an issue related to how the latest Apple sandbox requirements fit-in with Juce’s dynamic menu builder - specifically, getting an Open Recent menu item working…

Background: we’re looking to upload a product to the Mac App Store (MAS) in a day or 2 for first review.
This is a sandbox-enabled app.

However: Juce doesn’t currently support an Open Recent… menu item that works with the the MacOS Sandbox.

If you’re not familiar with sandbox apps - files in arbitrary locations (outside of the app sandbox area) can be opened by the app ONLY in direct response to the user having browsed for them with the standard system File Open menu.

So if the app wants to give user access to recently opened files (which might be outside of the sandbox), there is a special ultra-magic menu item in place that allows this to happen, trusted by the system.

Clearly, I’ve had to disable Recent Items support from the app for the sandboxed version of our own app.
However, we want to enable this because a) it is useful! b) I wouldn’t be surprised if the lack of the menu item leads to problems getting accepted on the store…

Some related links…:


The issue is that Open Recent needs special behaviour. If you’re using a xib-based menu, then all is fine and dandy - most of that support comes out of the box (the Open Recent menu item has special attributes - you can see these if you try it youself with a new project under XCode).

It might be possible to handle Open Recent items specially in Juce, using dynamic menu building special-case code (see here, but it is old - and I haven’t a clue if it works with the latest sandbox system!)

http://lapcatsoftware.com/blog/2007/07/10/working-without-a-nib-part-5-open-recent-menu/

So, when you’re doing things dynamically, like Juce does, all bets are off - I think it’ll take a fix to Juce (maybe changing Juce to use xib-based menus where required, as well as built-in Open Recent support).

Hoping somebody can advise - any Juce user wanting to target MacOS, selling into the Mac App Store with sandboxing enabled, is going to be affected by this!! … so it really does require a fast resolution! :slight_smile:

Best wishes,

Pete


#2

Unsurprisingly, I can’t find anything helpful in Apple’s docs about this. Did you also see this question:
http://stackoverflow.com/questions/9134784/mac-os-x-how-to-rebuild-menu-dependences

If it could be made to work by calling [NSDocumentController noteNewRecentDocumentURL] then that doesn’t sound too bad, but it’s hard to tell from the question whether he got it working or not.


#3

Hi Jules,

Ah, yes - that is the post I’d seen but couldn’t track down!

My guess is that he had to do with with an xib file :frowning:

If you create a new project with XCode, you’ll see that it automatically has a menu.

Look at the properties of the Open Recent item by looking inside the .xib file with a text editor - you’ll see it has certain properties. :slight_smile:
Does that give you the magic clue you need to patch it dynamically…???

Alternatively - what if Juce relied on a “standard, minimal” xib file - to which Juce appended non-standard items?
That’d probably be the way I’d approach it (and yes, that’d be a slight change required to existing projects…!)

Good luck!!

Pete


#4

Hi Jules,

Just wondering if you’d been able to make any progress with this. :slight_smile:

Best wishes,

Pete


#5

Did you see this?
http://lapcatsoftware.com/blog/2007/07/10/working-without-a-nib-part-5-open-recent-menu/

I had a go yesterday but got a bit stuck - I was trying to hack the menu code to make it auto-generate the Open Recent stuff, following the tips in that article, but had no luck. After calling the secret method to set the name, the menu did seem to become “special”, but I couldn’t persuade it to populate it with any files.

Need to do some other stuff today, will look again when I get chance…


#6

Hi Jules,

Good stuff - sounds like you might be getting somewhere - I hope! :slight_smile:

Of course, any app that calls secret/private methods will get rejected at review time… so my best guess is that Juce-based projects for the Mac App Store (which’ll be most of them!) will need to incorporate a .xib-based menu that Juce hooks-up to; plug-ins could continue to use the existing Juce approach as a plug-in wouldn’t have to use the “standard” native Mac menu.

Anyhow: we shall see what solution you end-up with. Best of luck. :slight_smile:

Best wishes,

Pete


#7

I’m really stumped by this one.

I’ll explain what I’ve tried so far, and maybe someone can suggest what I might be doing wrong!

I’ve just checked-in a change where I’ve added a method RecentlyOpenedFilesList::registerRecentFileNatively(), which tells OSX to add a file to its magic internal list. This all works fine, and I’ve modified the introjucer to use it, so all your recent files appear correctly on the introjucer’s dock menu for recent docs.

I downloaded this example code:
http://lapcatsoftware.com/blog/2007/07/10/working-without-a-nib-part-5-open-recent-menu/

…which I built, and it correctly creates a recent files menu… ok.

So I looked at how the menu was created in this example, and hacked juce_mac_MainMenu.mm to do the same thing. (This is a big dirty hack for testing, BTW, I’m not suggesting the final code should look like this):

[code] void addMenuItem (PopupMenu::MenuItemIterator& iter, NSMenu* menuToAddTo,
const int topLevelMenuId, const int topLevelIndex)
{
NSString* text = juceStringToNS (iter.itemName.upToFirstOccurrenceOf ("", false, true));

    if (text == nil)
        text = nsEmptyString();

    if (iter.isSeparator)
    {
        [menuToAddTo addItem: [NSMenuItem separatorItem]];
    }
    else if (iter.isSectionHeader)
    {
        NSMenuItem* item = [menuToAddTo addItemWithTitle: text
                                                  action: nil
                                           keyEquivalent: nsEmptyString()];

        [item setEnabled: false];
    }
    else if (iter.subMenu != nullptr)
    {
        if (iter.itemName.containsIgnoreCase ("recent"))
        {
            NSMenuItem* item = [menuToAddTo addItemWithTitle: NSLocalizedString (@"Open Recent", nil)
                                                      action: nil
                                               keyEquivalent: @""];

            NSMenu* openRecentMenu = [[[NSMenu alloc] initWithTitle: @"Open Recent"] autorelease];
            [openRecentMenu performSelector: @selector(_setMenuName:)
                                 withObject: @"NSRecentDocumentsMenu"];

            [menuToAddTo setSubmenu: openRecentMenu forItem: item];

            item = [openRecentMenu addItemWithTitle: NSLocalizedString(@"Clear Menu", nil)
                                             action: @selector(clearRecentDocuments:)
                                      keyEquivalent: @""];
        }
        else
        {
            NSMenuItem* item = [menuToAddTo addItemWithTitle: text
                                                      action: nil
                                               keyEquivalent: nsEmptyString()];

            [item setTag: iter.itemId];
            [item setEnabled: iter.isEnabled];

            NSMenu* sub = createMenu (*iter.subMenu, iter.itemName, topLevelMenuId, topLevelIndex);
            [sub setDelegate: nil];
            [menuToAddTo setSubmenu: sub forItem: item];
            [sub release];
        }
    }[/code]

…but it doesn’t work.

It does correctly create the “Clear” menu, and the clear menu works! But none of the recent items get populated in the menu. Looking at my code vs the Nibless example above, I can’t see anything at all that I’m doing differently, and I’ve tried all sorts of tweaks and hacks, but nothing seems to make any difference.

Got to move on and do some other stuff now, I’ll come back to this at some point, but am throwing it out there in case anyone has any clues in the meantime!


#8

Hi Jules,

Sure ain’t an easy one. :frowning:

I’d suggest you might want to consider using one of your two free Apple Tech Support tickets that you get each year with the Mac Developer programme.

I had to use one of mine a few weeks back, to get them to resolve an issue which was blocking one of my products getting through review; the support person I spoke with was great, responded quickly to mails and seemed keen and well informed! First time I’d used one of the tickets; I’m glad I did! :slight_smile:

If the “Open Recent” menu item can be done dynamically in a way that will pass code review for sandboxed apps (i.e. no private method calls!), that should give you a definitive solution one way or the other. I guess that they might tell you that it can only be done via an xib - but at least then we’d know, and could adapt (maybe you could auto-generate an xib from an xml file that describes the menu?).

Best wishes,

Pete


#9

Hi Guys,
may be its too late but I thought I should give you guys some hint (If its not solved yet)
Jules your Solution you wrote above will not work on App Store (What you might be doing wrong is you have to create all those Menu stuff in applicationWillFinishLoading not in applicationDidFinishLoading or anywhere after that). Then you have to call NSDocumentController’s – noteNewRecentDocumentURL: everytime you open/Save a document to retain access to it.
But as I said it will not work with App Store since you are using a private function.
What I did today was I made an nib file (Standard Template) then deleted all the menus from it leaving the File menu and OpenRecent menu in it. (Also the clear menu inside of Open Recent)
Then in my application Delegate’s applicationWillFinishLoading I load that nib file (BTW I copy that nib file in my app bundle) and ask for its items like this.
NSNib* menuNib = [[NSNib alloc ] initWithNibNamed:@“MainMenu” bundle:nil];
NSArray* array = [[NSArray alloc] init];
[menuNib instantiateNibWithOwner:NSApp topLevelObjects:&array];
[array retain];
// save this array somewhere access-able from your application
NSArray* urls = [[NSDocumentController sharedDocumentController] recentDocumentURLs]; //Do not know if this is required here but just doing it so that DController can read all its URLS saved in its plist file

now you have your Menu in that array you just need to get it and then add it to your own NSMenu item. That can be done anywhere in the code does not need to be done in ApplicationWill…

        NSMenuItem* recentItem;
        NSMenu *recentMenu;
        NSArray* array = getArraywhereever you saved it;
        NSArray* items = [[NSArray alloc] init];
        for (id object in array) {
                if([object isKindOfClass:[NSMenu class]])
                {
                        items = [object itemArray];
                        break;
                }
        }
        if(items != nil)
        {
                for (id object in items)
                {
                        if([object hasSubmenu])
                        {
                                NSMenu* subMenu = [object submenu];
                                NSArray* subMenuItems = [subMenu itemArray];
                                if([subMenuItems count])
                                {
                                        for (id subObject in subMenuItems)
                                        {
                                                if([subObject isKindOfClass:[NSMenuItem class]])
                                                {
                                                        recentItem = subObject;
                                                        if([subObject hasSubmenu])
                                                                recentMenu = [subObject menu];
                                                }
                                                
                                        }
                                }
                        }
                }
        }

Now you have your recentItem and reventMenu both.

[[recentItem menu] removeItem:recentItem];
//Remove it from the Nib menu first
[YourFileMenu addItem:(NSMenuItem*)recentItem];

Do not use as it is since we are doing some other stuff there and I have removed the stuff I thought not relevant so yeah Watch out for memory and erros.
I do not know what JUCE is and how it works and I do not even know if this will be applicable in your situation. I just came back here to give you guys a hint since I got a idea to solve this problem from this thread. So good luck and If you guys have already solved it Please just ignore this.
Thanks peteatjuce for the nib file idea you sure saved some hours for me :smiley:
Regards


#10

Completely brilliant - Jules, this sounds like the solution. :slight_smile:

Best4gotten - thank you very much for sharing your discovery - amazing how people on the Internet can work together by sharing info like this! :smiley:

Best wishes,

Pete


#11

Thanks for taking the time to share your tricks! Much appreciated!

Yep, I think that’s probably what was stopping it working. What a daft requirement… Thanks, I’ll have another go at this when I get chance!


#12

Nice one Jules! I presume you’re now using a (nearly-empty) NIB/XIB in the project, to provide the basic menu framework? I mention this, as of course we need to keep away from private APIs, to avoid apps getting rejected. :slight_smile:

Best wishes,

Pete


#13

Hello, before I dive into the delights of the sandbox, I’d like to know if this matter has been solved. Could please confirm Jules?
Are there other issues concerning the sandbox?
Many thanks


#14

This is the only issue I’ve found. It hasn’t yet prevented any of my own Juce-based apps being distributed through the Mac App Store (however - they don’t have any Open Recent menu item, therefore…!).

I guess Jules has been too busy with Projucer to look at this yet. :slight_smile:

Pete


#15

Right, I got this working without too much hacking.

Jules, maybe you could build this into Juce at some point?!

===

  1. Firstly, I used XCode to create a stripped-down .xib file in a new dummy project, named it
    FileOpenTemplateMenu.xib… and added that to my affected projects.

It contains ONLY a File… menu, containing the Open Recent … sub-menu … which are completely
untouched from what XCode created (but: I stripped-out all other menu items!)

  1. I patched the Juce source code… just look for MPC blocks - only a few new lines!

EDIT: commented-out a few lines which were causing some display problems for the File menu - see the comments in the code!

// MPC (begin)
void MyJuceHack_Mac_PrepareFileOpenTemplateMenu(void);
void MyJuceHack_Mac_AppendFileOpenMenu(const char *menuName, NSMenu *m);
// MPC (end)

BEGIN_JUCE_NAMESPACE

class JuceMainMenuHandler   : private MenuBarModel::Listener,
							  private DeletedAtShutdown
...

  void addSubMenu (NSMenu* parent, const PopupMenu& child,
					 const String& name, const int menuId, const int tag)
	{
		NSMenuItem* item = [parent addItemWithTitle: juceStringToNS (name)
											 action: nil
									  keyEquivalent: @""];
		[item setTag: tag];

		NSMenu* sub = createMenu (child, name, menuId, tag);
// MPC (begin)
// EDIT - commented this line out, as keeping it in sometimes made the first selection of the File...
// menu display *really* weirdly! The call doesn't see to add anything anyhow!
//  MyJuceHack_Mac_AppendFileOpenMenu(name.toUTF8().getAddress(), sub);
// MPC (end)

		[parent setSubmenu: sub forItem: item];
		[sub setAutoenablesItems: false];
		[sub release];
	}

...

	void updateSubMenu (NSMenuItem* parentItem, const PopupMenu& menuToCopy,
						const String& name, const int menuId, const int tag)
	{
		// Note: This method used to update the contents of the existing menu in-place, but that caused
		// weird side-effects which messed-up keyboard focus when switching between windows. By creating
		// a new menu and replacing the old one with it, that problem seems to be avoided..
		NSMenu* menu = [[NSMenu alloc] initWithTitle: juceStringToNS (name)];

		PopupMenu::MenuItemIterator iter (menuToCopy);
		while (iter.next())
			addMenuItem (iter, menu, menuId, tag);

// MPC (begin)
    MyJuceHack_Mac_AppendFileOpenMenu(name.toUTF8().getAddress(), menu);
// MPC (end)

		[menu setAutoenablesItems: false];
		[menu update];
		[parentItem setTag: tag];
		[parentItem setSubmenu: menu];
		[menu release];
	}

...

	NSMenu* createMenu (const PopupMenu menu,
						const String& menuName,
						const int topLevelMenuId,
						const int topLevelIndex)
	{
		NSMenu* m = [[NSMenu alloc] initWithTitle: juceStringToNS (menuName)];

		[m setAutoenablesItems: false];
		[m setDelegate: callback];

		PopupMenu::MenuItemIterator iter (menu);

		while (iter.next())
			addMenuItem (iter, m, topLevelMenuId, topLevelIndex);

// MPC (begin)
    MyJuceHack_Mac_AppendFileOpenMenu(menuName.toUTF8().getAddress(), m);
// MPC (end)

		[m update];
		return m;
	}

...

	// Since our app has no NIB, this initialises a standard app menu...
	void rebuildMainMenu (const PopupMenu* extraItems)
	{
		// this can't be used in a plugin!
		jassert (JUCEApplication::isStandaloneApp());

		if (JUCEApplication::getInstance() != nullptr)
		{
// MPC (begin)
      MyJuceHack_Mac_PrepareFileOpenTemplateMenu();
// MPC (end)

...
  1. I added a few new functions to my own code…
// Menu extension for Juce applications, to have File Open menu!
NSNib* menuNib = NULL;
NSArray* menuArray = NULL;
NSArray* menuUrls = NULL;

NSMenuItem* recentItem = NULL;
NSMenu *recentMenu = NULL;
  
void MyJuceHack_Mac_PrepareFileOpenTemplateMenu(void)
{
  if (menuNib == NULL)
  {
    menuNib = [[NSNib alloc ] initWithNibNamed:@"FileOpenTemplateMenu" bundle:nil];
    if (menuNib != NULL)
    {
      menuArray = [[NSArray alloc] init];
      [menuNib instantiateNibWithOwner:NSApp topLevelObjects:&menuArray];
      [menuArray retain];
      menuUrls = [[NSDocumentController sharedDocumentController] recentDocumentURLs];
    }
  }
}

void MyJuceHack_Mac_AppendFileOpenMenu(const char *menuName, NSMenu *m)
{
  // Prepare on first use!
  MyJuceHack_Mac_PrepareFileOpenTemplateMenu();
  
  if (im_utf8_strcmp((const im_utf8_t*)menuName, (const im_utf8_t*) "File") == 0)
  {
    // File menu... add-in the recent item, if available!
    if (recentItem == NULL)
    {
      // Not yet found it!
      if (menuArray != NULL)
      {
        NSArray* array = menuArray;
        NSArray* items = [[NSArray alloc] init];
        for (id object in array) {
          if([object isKindOfClass:[NSMenu class]])
          {
            items = [object itemArray];
            break;
          }
        }
        
        if(items != nil)
        {
          for (id object in items)
          {
            if([object hasSubmenu])
            {
              NSMenu* subMenu = [object submenu];
              NSArray* subMenuItems = [subMenu itemArray];
              if([subMenuItems count])
              {
                for (id subObject in subMenuItems)
                {
                  if([subObject isKindOfClass:[NSMenuItem class]])
                  {
                    recentItem = subObject;
                    NSString *title = recentItem.title;
                    const char *lpTitle = [title UTF8String];
                    printf ("SubMenuItem=%s\n", lpTitle);
                    if([subObject hasSubmenu])
                    {
                      recentMenu = [subObject menu];
                    }
                  }
                }
              }
            }
          }
        }
      }
    }

    if (recentItem != NULL)
    {
      //Remove it from any owning menu first!
      NSMenu *parent = [recentItem menu];
      if (parent != nil)
      {
        [parent removeItem:recentItem];
      }
      
      [recentItem retain]; // This seems to be essential, for some reason!
      
      NSMenu *myFileMenu = m;
      
      [myFileMenu addItem:recentItem];
    }
  }
}

void MyJuceHack_Mac_RegisterOpenMacDocument(const String &PPath)
{
  NSString *myPath =  [[NSString alloc] initWithUTF8String:PPath.toUTF8().getAddress()];
  NSURL *myUrl = [NSURL fileURLWithPath:myPath];
  [myPath release];
  
  [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:myUrl];
}
  1. I added calls in my code to MyJuceHack_Mac_RegisterOpenMacDocument whenever I’ve just
    saved or just opened a file on Mac…

  2. That is all - and it works!

Pete


#16

Just having a quick look at this, and struggling to get my tired brain around it… Do you think that to do this in a generic way, I’d have to add a method like

and then apps could add a nib to their project and call this to give it the nib’s name?

…or perhaps I could embed a suitable generic nib file as data, and load it using [NSNib initWithNibData], and handle the whole thing automatically without needing changes to apps?

BTW, your MyJuceHack_Mac_RegisterOpenMacDocument function isn’t needed - there’s already a method RecentlyOpenedFilesList::registerRecentFileNatively() that does the same job.


#17

Hi Jules!

Yes, sorry it was a bit of I hack: I just wanted to clearly isolate what I’d done, from the Juce master code. :slight_smile:

Yes, for sure, this should be handled in the main-line Juce code; we don’t really want the apps to handle this themselves.

I would suggest that the Jucer builds a project with a standard .xib file in it, called e.g. “JuceFileOpenRecentTemplate.xib”.
There would be no need for developers to re-create their own.
If you put this in the standard Juce distribution, developers could easily retrofit it to existing Juce projects. :slight_smile:

You’d probably want a project property or flag somewhere that the developer can use to tell Jules that they don’t want this menu item…?
Incidentally, does the latest Juce build-in a recent menu for other platforms that use a File… menu?

Thanks for the info on RecentlyOpenedFilesList::registerRecentFileNatively() … I’d been working on a version of Juce that doesn’t yet have that in the sources. :slight_smile:

Best wishes,

Pete


#18

Don’t suppose you could chuck me an example of a standard .xib file, could you? I’ve never done one before.

Hopefully it could be loaded from embedded data with [NSNib initWithNibData], so there’d be no need for any project changes or actual xib files at all…


#19

Sending it to you now. :slight_smile:

Yes, just open with initWithNibNamed …

NSNib* menuNib = NULL;
NSArray* menuArray = NULL;
NSArray* menuUrls = NULL;
  
void Intermorphic_Mac_PrepareFileOpenTemplateMenu(void)
{
  if (menuNib == NULL)
  {
    menuNib = [[NSNib alloc ] initWithNibNamed:@"FileOpenTemplateMenu" bundle:nil];
    if (menuNib != NULL)
    {
      menuArray = [[NSArray alloc] init];
      [menuNib instantiateNibWithOwner:NSApp topLevelObjects:&menuArray];
      [menuArray retain];
      menuUrls = [[NSDocumentController sharedDocumentController] recentDocumentURLs];
    }
  }
}

Best wishes as always,

Pete


#20

FYI, I’ve just checked in some code that should do this now.

It involves some introjucer hackery to provide a hidden .xib file, so you need to re-save your project, and you need to add a parameter to MenuBarModel::setMacMainMenu() to tell it which menu to replace.