FileChooser and FilePreviewComponent on OSX


#1

Hi all. I want to use a FilePreviewComponent (which can play back audio samples through system out) with a FileChooser. When I use a preview component on OS X I get the JUCE file selector instead of the native one. However I would much prefer to use the native one and have a juce component embedded, similar to how it works on windows. Digging through the Apple Documentation I found that NSSavePanel has a method called setAccessoryView: that should theoretically allow this. This makes me wonder why that is not implemented in JUCE yet. Has it maybe been attempted but there is something in Cocoa that makes it impossible? Is this on the todo list for JUCE?

I am thinking about writing a patch myself, but things might get messy fast, so I wondered if anyone has attempted this before. Should I end up writing it myself I would be more than willing to share the result.

I just think a custom file chooser always looks very alien - even though the JUCE one is visually matching my application with my custom lookAndFeel. It feels to me like this is something that really belongs to the OS and not to individual running apps.

setAccessoryView:

#2

I gave it a go and this is patch for juce_mac_FileChooser.mm is what I came up with so far. It seems to work for me, but I’m not sure everything cleans up correctly with the addToDesktop call.

I also wondered whether there is not a simpler way to deal with the Cocoa Class naming fiasko (ObjCClass…). I vaguely remember doing something with the Preprocessor that would enable unique obj-c classnames for each project without having to do everything with C.

A bit of an issue with the whole thing is that windows uses a layout with the preview component on the right side while OSX wants to but it below the file selector panel. So using the same component seems a bit tricky as one need to be tall and the other wide. Nevertheless I prefer this to using the JUCE custom panel.

diff --git a/modules/juce_gui_basics/native/juce_mac_FileChooser.mm b/modules/juce_gui_basics/native/juce_mac_FileChooser.mm
index f256067..20fbba8 100644
--- a/modules/juce_gui_basics/native/juce_mac_FileChooser.mm
+++ b/modules/juce_gui_basics/native/juce_mac_FileChooser.mm
@@ -29,9 +29,11 @@ struct FileChooserDelegateClass  : public ObjCClass <NSObject>
     FileChooserDelegateClass()  : ObjCClass <NSObject> ("JUCEFileChooser_")
     {
         addIvar<StringArray*> ("filters");
+        addIvar<FilePreviewComponent*> ("filePreviewComponent");
 
         addMethod (@selector (dealloc),                   dealloc,            "v@:");
         addMethod (@selector (panel:shouldShowFilename:), shouldShowFilename, "c@:@@");
+        addMethod (@selector (panelSelectionDidChange:), selectionDidChange, "c@");
 
        #if defined (MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
         addProtocol (@protocol (NSOpenSavePanelDelegate));
@@ -45,6 +47,11 @@ struct FileChooserDelegateClass  : public ObjCClass <NSObject>
         object_setInstanceVariable (self, "filters", filters);
     }
 
+    static void setFilePreviewComponent (id self, FilePreviewComponent* previewComponent)
+    {
+        object_setInstanceVariable (self, "filePreviewComponent", previewComponent);
+    }
+
 private:
     static void dealloc (id self, SEL)
     {
@@ -81,6 +88,33 @@ private:
         return f.isDirectory()
                  && ! [[NSWorkspace sharedWorkspace] isFilePackageAtPath: filename];
     }
+    
+    static void selectionDidChange (id self, SEL, id sender) {
+        FilePreviewComponent* const previewComp = getIvar<FilePreviewComponent*> (self, "filePreviewComponent");
+        String filePath;
+        if ([sender isKindOfClass:[NSOpenPanel class]])
+        {
+            NSOpenPanel* panel = (NSOpenPanel*)sender;
+            NSArray* urls = [panel URLs];
+            for (unsigned int i = 0; i < [urls count]; ++i)
+            {
+                if (filePath.isEmpty())
+                {
+                    filePath = nsStringToJuce([[urls objectAtIndex: i] path]);
+                }
+                else
+                {
+                    filePath += " " + nsStringToJuce([[urls objectAtIndex: i] path]);
+                }
+            }
+        }
+        else if ([sender isKindOfClass:[NSSavePanel class]])
+        {
+            NSSavePanel* panel = (NSSavePanel*)sender;
+            filePath = nsStringToJuce([[panel URL] path]);
+        }
+        previewComp->selectedFileChanged(File(filePath));
+    }
 };
 
 static NSMutableArray* createAllowedTypesArray (const StringArray& filters)
@@ -113,7 +147,7 @@ void FileChooser::showPlatformDialog (Array<File>& results,
                                       bool isSaveDialogue,
                                       bool /*warnAboutOverwritingExistingFiles*/,
                                       bool selectMultipleFiles,
-                                      FilePreviewComponent* /*extraInfoComponent*/)
+                                      FilePreviewComponent* extraInfoComponent)
 {
     JUCE_AUTORELEASEPOOL
     {
@@ -150,6 +184,16 @@ void FileChooser::showPlatformDialog (Array<File>& results,
             [openPanel setAllowsMultipleSelection: selectMultipleFiles];
             [openPanel setResolvesAliases: YES];
         }
+        
+        if (extraInfoComponent != nullptr)
+        {
+            NSView* view = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, extraInfoComponent->getWidth(), extraInfoComponent->getHeight())] autorelease];
+            extraInfoComponent->addToDesktop(0, (void*)view);
+            extraInfoComponent->setVisible(view);
+            FileChooserDelegateClass::setFilePreviewComponent(delegate, extraInfoComponent);
+
+            [panel setAccessoryView:view];
+        }
 
         [panel setDelegate: delegate];
 

#3

Thanks! I'll take a look asap.

BTW a lot of the reason for doing obj-C classes using C++ was because of other issues involving startup and shutdown code that the compiler generates, and because LLVM's JIT compiler couldn't handle obj-C classes, which meant that the Projucer was impossible without doing it this way.


#4

Awesome, I just saw your solution in the JUCE changelog. Of course it's much better and more complete than what I did.  Thanks heaps! 

I guess the C way is fine as long as you don't need lots of Obj-C classes ;)

 

I saw you wrote in the patch:

  static void panelSelectionDidChange (id self, SEL, id sender)
    {
        // NB: would need to extend FilePreviewComponent to handle the full list rather than just the first one

 

I noticed on windows multi-selection does send multiple selected files to the preview component like so: C:\yada\yada\"yada1.wav yada2.ogg yada3.aif". I guess that is just what comes from windows, but it does allow for multi-selection to somewhat work.

After reading your comment I think it's a bug in the windows version. Having multiselection for the filepreviewcomponent would be nice - even if the preview can only handle one file. It would give the preview the chance to display a message for multi-selection explaining the problem to the user.


#5

Yes, it'd make no sense to try to send a bunch of space-separated filenames, because the parameter is a File object - it only expects a single path. I didn't know the win32 version was doing that, but if so, it is a bug in the win32 one.