support for file-type popup, compatible for 10.4+, solves #12429
[wxWidgets.git] / src / osx / cocoa / filedlg.mm
1 /////////////////////////////////////////////////////////////////////////////
2 // Name:        src/cocoa/filedlg.mm
3 // Purpose:     wxFileDialog for wxCocoa
4 // Author:      Ryan Norton
5 // Modified by:
6 // Created:     2004-10-02
7 // RCS-ID:      $Id: filedlg.mm 40007 2006-07-05 13:10:46Z SC $
8 // Copyright:   (c) Ryan Norton
9 // Licence:     wxWindows licence
10 /////////////////////////////////////////////////////////////////////////////
11
12 // ============================================================================
13 // declarations
14 // ============================================================================
15
16 // ----------------------------------------------------------------------------
17 // headers
18 // ----------------------------------------------------------------------------
19
20 // For compilers that support precompilation, includes "wx.h".
21 #include "wx/wxprec.h"
22
23 #if wxUSE_FILEDLG
24
25 #include "wx/filedlg.h"
26
27 #ifndef WX_PRECOMP
28     #include "wx/msgdlg.h"
29     #include "wx/app.h"
30 #endif
31
32 #include "wx/filename.h"
33 #include "wx/tokenzr.h"
34
35 #include "wx/osx/private.h"
36 #include "wx/sysopt.h"
37
38 // ============================================================================
39 // implementation
40 // ============================================================================
41
42 // Open Items:
43 // - parameter support for descending into packages as directories (setTreatsFilePackagesAsDirectories)
44 // - as setAllowedFileTypes is only functional for NSOpenPanel on 10.6+, on earlier systems, the file
45 // type choice will not be shown, but all possible file items will be shown, if a popup must be working
46 // then the delegate method - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename will have to
47 // be implemented
48
49 @interface wxOpenPanelDelegate : NSObject wxOSX_10_6_AND_LATER(<NSOpenSavePanelDelegate>)
50 {
51     wxFileDialog* _dialog;
52 }
53
54 - (wxFileDialog*) fileDialog;
55 - (void) setFileDialog:(wxFileDialog*) dialog;
56
57 - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename;
58
59 @end
60
61 @implementation wxOpenPanelDelegate
62
63 - (id) init
64 {
65     [super init];
66     _dialog = NULL;
67     return self;
68 }
69
70 - (wxFileDialog*) fileDialog
71 {
72     return _dialog;
73 }
74
75 - (void) setFileDialog:(wxFileDialog*) dialog
76 {
77     _dialog = dialog;
78 }
79
80 - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename
81 {
82     BOOL showObject = YES;
83     
84     NSString* resolvedLink = [[NSFileManager defaultManager] pathContentOfSymbolicLinkAtPath:filename];
85     if ( resolvedLink != nil )
86         filename = resolvedLink;
87     
88     NSDictionary* fileAttribs = [[NSFileManager defaultManager]
89                                  fileAttributesAtPath:filename traverseLink:YES];
90     if (fileAttribs)
91     {
92         // check for packages
93         if ([NSFileTypeDirectory isEqualTo:[fileAttribs objectForKey:NSFileType]])
94         {
95             if ([[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename] == NO)
96                 showObject = YES;    // it's a folder, OK to show
97             else
98             {
99                 // it's a packaged directory, apply check
100                 wxCFStringRef filecf([filename retain]);
101                 showObject = _dialog->CheckFile(filecf.AsString());  
102             }
103         }
104         else
105         {
106             // the code above only solves links, not aliases, do this here:
107             
108             NSString* resolvedAlias = nil;
109             
110             CFURLRef url = CFURLCreateWithFileSystemPath (kCFAllocatorDefault, 
111                                                           (CFStringRef)filename, 
112                                                           kCFURLPOSIXPathStyle,
113                                                           NO); 
114             if (url != NULL) 
115             {
116                 FSRef fsRef; 
117                 if (CFURLGetFSRef(url, &fsRef)) 
118                 {
119                     Boolean targetIsFolder, wasAliased;
120                     OSErr err = FSResolveAliasFile (&fsRef, true, &targetIsFolder, &wasAliased);
121                     
122                     if ((err == noErr) && wasAliased) 
123                     {
124                         CFURLRef resolvedUrl = CFURLCreateFromFSRef(kCFAllocatorDefault,  &fsRef);
125                         if (resolvedUrl != NULL) 
126                         {
127                             resolvedAlias = (NSString*) CFURLCopyFileSystemPath(resolvedUrl,
128                                                                                kCFURLPOSIXPathStyle); 
129                             CFRelease(resolvedUrl);
130                         }
131                     } 
132                 }
133                 CFRelease(url);
134             }
135
136             if (resolvedAlias != nil) 
137             {
138                 // recursive call
139                 [resolvedAlias autorelease];
140                 showObject = [self panel:sender shouldShowFilename:resolvedAlias];
141             }
142             else
143             {
144                 wxCFStringRef filecf([filename retain]);
145                 showObject = _dialog->CheckFile(filecf.AsString());  
146             }
147         }
148     }
149
150     return showObject;    
151 }
152
153 @end
154
155 IMPLEMENT_CLASS(wxFileDialog, wxFileDialogBase)
156
157 wxFileDialog::wxFileDialog(
158     wxWindow *parent, const wxString& message,
159     const wxString& defaultDir, const wxString& defaultFileName, const wxString& wildCard,
160     long style, const wxPoint& pos, const wxSize& sz, const wxString& name)
161     : wxFileDialogBase(parent, message, defaultDir, defaultFileName, wildCard, style, pos, sz, name)
162 {
163 }
164
165 bool wxFileDialog::SupportsExtraControl() const
166 {
167     return true;
168 }
169
170 NSArray* GetTypesFromExtension( const wxString extensiongroup, wxArrayString& extensions )
171 {
172     NSMutableArray* types = nil;
173     extensions.Clear();
174
175     wxStringTokenizer tokenizer( extensiongroup, wxT(";") ) ;
176     while ( tokenizer.HasMoreTokens() )
177     {
178         wxString extension = tokenizer.GetNextToken() ;
179         // Remove leading '*'
180         if ( extension.length() && (extension.GetChar(0) == '*') )
181             extension = extension.Mid( 1 );
182
183         // Remove leading '.'
184         if ( extension.length() && (extension.GetChar(0) == '.') )
185             extension = extension.Mid( 1 );
186
187         // Remove leading '*', this is for handling *.*
188         if ( extension.length() && (extension.GetChar(0) == '*') )
189             extension = extension.Mid( 1 );
190
191         if ( extension.IsEmpty() )
192         {
193             extensions.Clear();
194             [types release];
195             types = nil;
196             return nil;
197         }
198
199         if ( types == nil )
200             types = [[NSMutableArray alloc] init];
201
202         extensions.Add(extension.Lower());
203         wxCFStringRef cfext(extension);
204         [types addObject: (NSString*)cfext.AsNSString()  ];
205 #if 0
206         // add support for classic fileType / creator here
207         wxUint32 fileType, creator;
208         // extension -> mactypes
209 #endif
210     }
211     [types autorelease];
212     return types;
213 }
214
215 NSArray* GetTypesFromFilter( const wxString& filter, wxArrayString& names, wxArrayString& extensiongroups )
216 {
217     NSMutableArray* types = nil;
218     bool allowAll = false;
219
220     names.Clear();
221     extensiongroups.Clear();
222
223     if ( !filter.empty() )
224     {
225         wxStringTokenizer tokenizer( filter, wxT("|") );
226         int numtokens = (int)tokenizer.CountTokens();
227         if(numtokens == 1)
228         {
229             // we allow for compatibility reason to have a single filter expression (like *.*) without
230             // an explanatory text, in that case the first part is name and extension at the same time
231             wxString extension = tokenizer.GetNextToken();
232             names.Add( extension );
233             extensiongroups.Add( extension );
234         }
235         else
236         {
237             int numextensions = numtokens / 2;
238             for(int i = 0; i < numextensions; i++)
239             {
240                 wxString name = tokenizer.GetNextToken();
241                 wxString extension = tokenizer.GetNextToken();
242                 names.Add( name );
243                 extensiongroups.Add( extension );
244             }
245         }
246
247         const size_t extCount = extensiongroups.GetCount();
248         wxArrayString extensions;
249         for ( size_t i = 0 ; i < extCount; i++ )
250         {
251             NSArray* exttypes = GetTypesFromExtension(extensiongroups[i], extensions);
252             if ( exttypes != nil )
253             {
254                 if ( allowAll == false )
255                 {
256                     if ( types == nil )
257                         types = [[NSMutableArray alloc] init];
258
259                     [types addObjectsFromArray:exttypes];
260                 }
261             }
262             else
263             {
264                 allowAll = true;
265                 [types release];
266                 types = nil;
267             }
268         }
269     }
270     [types autorelease];
271     return types;
272 }
273
274 void wxFileDialog::ShowWindowModal()
275 {
276     wxCFStringRef cf( m_message );
277     wxCFStringRef dir( m_dir );
278     wxCFStringRef file( m_fileName );
279
280     wxNonOwnedWindow* parentWindow = NULL;
281     
282     m_modality = wxDIALOG_MODALITY_WINDOW_MODAL;
283
284     if (GetParent())
285         parentWindow = dynamic_cast<wxNonOwnedWindow*>(wxGetTopLevelParent(GetParent()));
286
287     wxASSERT_MSG(parentWindow, "Window modal display requires parent.");
288
289     NSArray* types = GetTypesFromFilter( m_wildCard, m_filterNames, m_filterExtensions ) ;
290     if ( HasFlag(wxFD_SAVE) )
291     {
292         NSSavePanel* sPanel = [NSSavePanel savePanel];
293
294         SetupExtraControls(sPanel);
295
296         // makes things more convenient:
297         [sPanel setCanCreateDirectories:YES];
298         [sPanel setMessage:cf.AsNSString()];
299         // if we should be able to descend into pacakges we must somehow
300         // be able to pass this in
301         [sPanel setTreatsFilePackagesAsDirectories:NO];
302         [sPanel setCanSelectHiddenExtension:YES];
303         [sPanel setAllowedFileTypes:types];
304         [sPanel setAllowsOtherFileTypes:NO];
305         
306         NSWindow* nativeParent = parentWindow->GetWXWindow();
307         ModalDialogDelegate* sheetDelegate = [[ModalDialogDelegate alloc] init];
308         [sheetDelegate setImplementation: this];
309         [sPanel beginSheetForDirectory:dir.AsNSString() file:file.AsNSString()
310             modalForWindow: nativeParent modalDelegate: sheetDelegate
311             didEndSelector: @selector(sheetDidEnd:returnCode:contextInfo:)
312             contextInfo: nil];
313     }
314     else 
315     {
316         NSOpenPanel* oPanel = [NSOpenPanel openPanel];
317         
318         SetupExtraControls(oPanel);
319
320         [oPanel setTreatsFilePackagesAsDirectories:NO];
321         [oPanel setCanChooseDirectories:NO];
322         [oPanel setResolvesAliases:YES];
323         [oPanel setCanChooseFiles:YES];
324         [oPanel setMessage:cf.AsNSString()];
325         [oPanel setAllowsMultipleSelection: (HasFlag(wxFD_MULTIPLE) ? YES : NO )];
326         
327         NSWindow* nativeParent = parentWindow->GetWXWindow();
328         ModalDialogDelegate* sheetDelegate = [[ModalDialogDelegate alloc] init];
329         [sheetDelegate setImplementation: this];
330         [oPanel beginSheetForDirectory:dir.AsNSString() file:file.AsNSString()
331             types: types modalForWindow: nativeParent
332             modalDelegate: sheetDelegate
333             didEndSelector: @selector(sheetDidEnd:returnCode:contextInfo:)
334             contextInfo: nil];
335     }
336 }
337
338 // Create a panel with the file type drop down list
339 // If extra controls need to be added (see wxFileDialog::SetExtraControlCreator), add
340 // them to the panel as well
341 // Returns the newly created wxPanel
342
343 wxWindow* wxFileDialog::CreateFilterPanel(wxWindow *extracontrol)
344 {
345     wxPanel *extrapanel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
346     wxBoxSizer *verticalSizer = new wxBoxSizer(wxVERTICAL);
347     extrapanel->SetSizer(verticalSizer);
348     
349     // the file type control
350     {
351         wxBoxSizer *horizontalSizer = new wxBoxSizer(wxHORIZONTAL);
352         verticalSizer->Add(horizontalSizer, 0, wxEXPAND, 0);
353         wxStaticText *stattext = new wxStaticText( extrapanel, wxID_ANY, _("File type:") );
354         horizontalSizer->Add(stattext, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);
355         m_filterChoice = new wxChoice(extrapanel, wxID_ANY);
356         horizontalSizer->Add(m_filterChoice, 1, wxALIGN_CENTER_VERTICAL|wxALL, 5);
357         m_filterChoice->Append(m_filterNames);
358         if( m_filterNames.GetCount() > 0)
359         {
360             if ( m_firstFileTypeFilter >= 0 )
361                 m_filterChoice->SetSelection(m_firstFileTypeFilter);
362         }
363         m_filterChoice->Connect(wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler(wxFileDialog::OnFilterSelected), NULL, this);
364     }
365         
366     if(extracontrol)
367     {
368         wxBoxSizer *horizontalSizer = new wxBoxSizer(wxHORIZONTAL);
369         verticalSizer->Add(horizontalSizer, 0, wxEXPAND, 0);
370
371         extracontrol->Reparent(extrapanel);
372         horizontalSizer->Add(extracontrol);
373     }
374
375     verticalSizer->Layout();
376     verticalSizer->SetSizeHints(extrapanel);
377     return extrapanel;
378 }
379
380 // An item has been selected in the file filter wxChoice:
381 void wxFileDialog::OnFilterSelected( wxCommandEvent &WXUNUSED(event) )
382 {
383     int index = m_filterChoice->GetSelection();
384
385     NSArray* types = GetTypesFromExtension(m_filterExtensions[index],m_currentExtensions);
386     NSSavePanel* panel = (NSSavePanel*) GetWXWindow();
387     if ( m_delegate )
388         [panel validateVisibleColumns];
389     else
390         [panel setAllowedFileTypes:types];
391 }
392
393 bool wxFileDialog::CheckFile( const wxString& filename )
394 {
395     if ( m_currentExtensions.GetCount() == 0 )
396         return true;
397     
398     wxString ext = filename.AfterLast('.').Lower();
399     
400     for ( size_t i = 0; i < m_currentExtensions.GetCount(); ++i )
401     {
402         if ( ext == m_currentExtensions[i] )
403             return true;
404     }
405     return false;
406 }
407
408 void wxFileDialog::SetupExtraControls(WXWindow nativeWindow)
409 {
410     NSSavePanel* panel = (NSSavePanel*) nativeWindow;
411     
412     wxNonOwnedWindow::Create( GetParent(), nativeWindow );
413     wxWindow* extracontrol = NULL;
414     if ( HasExtraControlCreator() )
415     {
416         CreateExtraControl();
417         extracontrol = GetExtraControl();
418     }
419
420     NSView* accView = nil;
421     m_delegate = nil;
422
423     if ( m_useFileTypeFilter )
424     {
425         m_filterPanel = CreateFilterPanel(extracontrol);
426         accView = m_filterPanel->GetHandle();
427         if( HasFlag(wxFD_OPEN) )
428         {
429             if ( 1 /* UMAGetSystemVersion() < 0x1060 */ )
430             {
431                 wxOpenPanelDelegate* del = [[wxOpenPanelDelegate alloc]init];
432                 [del setFileDialog:this];
433                 [panel setDelegate:del];
434                 m_delegate = del;
435             }
436         }
437     }
438     else
439     {
440         m_filterPanel = NULL;
441         m_filterChoice = NULL;
442         if ( extracontrol != nil )
443             accView = extracontrol->GetHandle();
444     }
445
446     if ( accView != nil )
447     {
448         [accView removeFromSuperview];
449         [panel setAccessoryView:accView];
450     }
451     else
452     {
453         [panel setAccessoryView:nil];
454     }
455 }
456
457 int wxFileDialog::ShowModal()
458 {
459     wxCFStringRef cf( m_message );
460
461     wxCFStringRef dir( m_dir );
462     wxCFStringRef file( m_fileName );
463
464     m_path = wxEmptyString;
465     m_fileNames.Clear();
466     m_paths.Clear();
467     // since we don't support retrieving the matching filter
468     m_filterIndex = -1;
469
470     wxNonOwnedWindow* parentWindow = NULL;
471     int returnCode = -1;
472
473     if (GetParent())
474     {
475         parentWindow = dynamic_cast<wxNonOwnedWindow*>(wxGetTopLevelParent(GetParent()));
476     }
477
478
479     NSArray* types = GetTypesFromFilter( m_wildCard, m_filterNames, m_filterExtensions ) ;
480
481     m_useFileTypeFilter = m_filterExtensions.GetCount() > 1;
482
483     if( HasFlag(wxFD_OPEN) )
484     {
485         if ( !(wxSystemOptions::HasOption( wxOSX_FILEDIALOG_ALWAYS_SHOW_TYPES ) && (wxSystemOptions::GetOptionInt( wxOSX_FILEDIALOG_ALWAYS_SHOW_TYPES ) == 1)) )
486             m_useFileTypeFilter = false;            
487     }
488
489     m_firstFileTypeFilter = -1;
490     
491     if ( m_useFileTypeFilter )
492     {
493         types = nil;
494         bool useDefault = true;
495         for ( size_t i = 0; i < m_filterExtensions.GetCount(); ++i )
496         {
497             types = GetTypesFromExtension(m_filterExtensions[i], m_currentExtensions);
498             if ( m_currentExtensions.GetCount() == 0 )
499             {
500                 useDefault = false;
501                 m_firstFileTypeFilter = i;
502                 break;
503             }
504             
505             for ( size_t j = 0; j < m_currentExtensions.GetCount(); ++j )
506             {
507                 if ( m_fileName.EndsWith(m_currentExtensions[j]) )
508                 {
509                     m_firstFileTypeFilter = i;
510                     useDefault = false;
511                     break;
512                 }
513             }
514             if ( !useDefault )
515                 break;
516         }
517         if ( useDefault )
518         {
519             types = GetTypesFromExtension(m_filterExtensions[0], m_currentExtensions);
520             m_firstFileTypeFilter = 0;
521         }
522     }
523
524     if ( HasFlag(wxFD_SAVE) )
525     {
526         NSSavePanel* sPanel = [NSSavePanel savePanel];
527
528         SetupExtraControls(sPanel);
529
530         // makes things more convenient:
531         [sPanel setCanCreateDirectories:YES];
532         [sPanel setMessage:cf.AsNSString()];
533         // if we should be able to descend into pacakges we must somehow
534         // be able to pass this in
535         [sPanel setTreatsFilePackagesAsDirectories:NO];
536         [sPanel setCanSelectHiddenExtension:YES];
537         [sPanel setAllowedFileTypes:types];
538         [sPanel setAllowsOtherFileTypes:NO];
539
540         if ( HasFlag(wxFD_OVERWRITE_PROMPT) )
541         {
542         }
543
544         returnCode = [sPanel runModalForDirectory:dir.AsNSString() file:file.AsNSString() ];
545         ModalFinishedCallback(sPanel, returnCode);
546     }
547     else
548     {
549         NSOpenPanel* oPanel = [NSOpenPanel openPanel];
550         
551         SetupExtraControls(oPanel);
552                 
553         [oPanel setTreatsFilePackagesAsDirectories:NO];
554         [oPanel setCanChooseDirectories:NO];
555         [oPanel setResolvesAliases:YES];
556         [oPanel setCanChooseFiles:YES];
557         [oPanel setMessage:cf.AsNSString()];
558         [oPanel setAllowsMultipleSelection: (HasFlag(wxFD_MULTIPLE) ? YES : NO )];
559
560         if ( UMAGetSystemVersion() < 0x1060 )
561         {
562             returnCode = [oPanel runModalForDirectory:dir.AsNSString()
563                                                  file:file.AsNSString() types:(m_delegate == nil ? types : nil)];
564         }
565         else 
566         {
567             [oPanel setAllowedFileTypes: (m_delegate == nil ? types : nil)];
568             [oPanel setDirectoryURL:[NSURL fileURLWithPath:dir.AsNSString() 
569                                                isDirectory:YES]];
570             returnCode = [oPanel runModal];
571         }
572
573         ModalFinishedCallback(oPanel, returnCode);
574     }
575
576     return GetReturnCode();
577 }
578
579 void wxFileDialog::ModalFinishedCallback(void* panel, int returnCode)
580 {
581     int result = wxID_CANCEL;
582     if (HasFlag(wxFD_SAVE))
583     {
584         if (returnCode == NSOKButton )
585         {
586             NSSavePanel* sPanel = (NSSavePanel*)panel;
587             result = wxID_OK;
588
589             m_path = wxCFStringRef::AsString([sPanel filename]);
590             m_fileName = wxFileNameFromPath(m_path);
591             m_dir = wxPathOnly( m_path );
592         }
593     }
594     else
595     {
596         NSOpenPanel* oPanel = (NSOpenPanel*)panel;
597         if (returnCode == NSOKButton )
598         {
599             panel = oPanel;
600             result = wxID_OK;
601             NSArray* filenames = [oPanel filenames];
602             for ( size_t i = 0 ; i < [filenames count] ; ++ i )
603             {
604                 wxString fnstr = wxCFStringRef::AsString([filenames objectAtIndex:i]);
605                 m_paths.Add( fnstr );
606                 m_fileNames.Add( wxFileNameFromPath(fnstr) );
607                 if ( i == 0 )
608                 {
609                     m_path = fnstr;
610                     m_fileName = wxFileNameFromPath(fnstr);
611                     m_dir = wxPathOnly( fnstr );
612                 }
613             }
614         }
615         if ( m_delegate )
616         {
617             [oPanel setDelegate:nil];
618             [m_delegate release];
619             m_delegate = nil;
620         }
621     }
622     SetReturnCode(result);
623     
624     if (GetModality() == wxDIALOG_MODALITY_WINDOW_MODAL)
625         SendWindowModalDialogEvent ( wxEVT_WINDOW_MODAL_DIALOG_CLOSED  );
626     
627     UnsubclassWin();
628     [(NSSavePanel*) panel setAccessoryView:nil];
629 }
630
631 #endif // wxUSE_FILEDLG