/// // Copied from wxWidgets 3.0.2 and modified to support additional features // ///////////////////////////////////////////////////////////////////////////// // Name: src/cocoa/filedlg.mm // Purpose: wxFileDialog for wxCocoa // Author: Ryan Norton // Modified by: Leland Lucius // Created: 2004-10-02 // Copyright: (c) Ryan Norton // Licence: wxWindows licence ///////////////////////////////////////////////////////////////////////////// // ============================================================================ // declarations // ============================================================================ // ---------------------------------------------------------------------------- // headers // ---------------------------------------------------------------------------- #include "Internat.h" #include "../FileDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // ============================================================================ // implementation // ============================================================================ @interface OSPanelDelegate : NSObject { FileDialog* _dialog; } - (FileDialog*) fileDialog; - (void) setFileDialog:(FileDialog*) dialog; - (void)panel:(id)sender didChangeToDirectoryURL:(NSURL *)url; - (void)panelSelectionDidChange:(id)sender; - (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError * _Nullable *)outError; - (void)viewResized:(NSNotification *)notification; @end @implementation OSPanelDelegate - (void)viewResized:(NSNotification *)notification { _dialog->DoViewResized([notification object]); } - (id) init { if ( self = [super init] ) { _dialog = NULL; } return self; } - (FileDialog*) fileDialog { return _dialog; } - (void) setFileDialog:(FileDialog*) dialog { _dialog = dialog; } - (void)panel:(id)sender didChangeToDirectoryURL:(NSURL *)url AVAILABLE_MAC_OS_X_VERSION_10_6_AND_LATER { wxString path = wxCFStringRef::AsStringWithNormalizationFormC( [url path] ); _dialog->DoSendFolderChangedEvent(sender, path); } - (void)panelSelectionDidChange:(id)sender AVAILABLE_MAC_OS_X_VERSION_10_3_AND_LATER { _dialog->DoSendSelectionChangedEvent(sender); } // Do NOT remove this method. For an explanation, refer to: // // http://bugzilla.audacityteam.org/show_bug.cgi?id=2371 // - (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError * _Nullable *)outError; { // We handle filename validation after the panel closes return YES; } @end wxIMPLEMENT_CLASS(FileDialog, FileDialogBase) FileDialog::FileDialog() : FileDialogBase() { Init(); } FileDialog::FileDialog(wxWindow *parent, const wxString& message, const wxString& defaultDir, const wxString& defaultFile, const wxString& wildCard, long style, const wxPoint& pos, const wxSize& sz, const wxString& name) : FileDialogBase() { Init(); Create(parent,message,defaultDir,defaultFile,wildCard,style,pos,sz,name); } void FileDialog::Init() { m_filterIndex = -1; m_delegate = nil; m_filterPanel = NULL; m_filterChoice = NULL; } void FileDialog::Create( wxWindow *parent, const wxString& message, const wxString& defaultDir, const wxString& defaultFileName, const wxString& wildCard, long style, const wxPoint& pos, const wxSize& sz, const wxString& name) { FileDialogBase::Create(parent, message, defaultDir, defaultFileName, wildCard, style, pos, sz, name); } FileDialog::~FileDialog() { } bool FileDialog::SupportsExtraControl() const { return true; } NSArray* GetTypesFromExtension( const wxString extensiongroup, wxArrayString& extensions ) { NSMutableArray* types = nil; extensions.Clear(); wxStringTokenizer tokenizer( extensiongroup, wxT(";") ) ; while ( tokenizer.HasMoreTokens() ) { wxString extension = tokenizer.GetNextToken() ; // Remove leading '*' if ( extension.length() && (extension.GetChar(0) == '*') ) extension = extension.Mid( 1 ); // Remove leading '.' if ( extension.length() && (extension.GetChar(0) == '.') ) extension = extension.Mid( 1 ); // Remove leading '*', this is for handling *.* if ( extension.length() && (extension.GetChar(0) == '*') ) extension = extension.Mid( 1 ); if ( extension.IsEmpty() ) { extensions.Clear(); [types release]; types = nil; return nil; } if ( types == nil ) types = [[NSMutableArray alloc] init]; extensions.Add(extension.Lower()); wxCFStringRef cfext(extension); [types addObject: (NSString*)cfext.AsNSString() ]; } [types autorelease]; return types; } NSArray* GetTypesFromFilter( const wxString& filter, wxArrayString& names, wxArrayString& extensiongroups ) { NSMutableArray* types = nil; bool allowAll = false; names.Clear(); extensiongroups.Clear(); if ( !filter.empty() ) { wxStringTokenizer tokenizer( filter, wxT("|") ); int numtokens = (int)tokenizer.CountTokens(); if(numtokens == 1) { // we allow for compatibility reason to have a single filter expression (like *.*) without // an explanatory text, in that case the first part is name and extension at the same time wxString extension = tokenizer.GetNextToken(); names.Add( extension ); extensiongroups.Add( extension ); } else { int numextensions = numtokens / 2; for(int i = 0; i < numextensions; i++) { wxString name = tokenizer.GetNextToken(); wxString extension = tokenizer.GetNextToken(); names.Add( name ); extensiongroups.Add( extension ); } } const size_t extCount = extensiongroups.GetCount(); wxArrayString extensions; for ( size_t i = 0 ; i < extCount; i++ ) { NSArray* exttypes = GetTypesFromExtension(extensiongroups[i], extensions); if ( exttypes != nil ) { if ( allowAll == false ) { if ( types == nil ) types = [[NSMutableArray alloc] init]; [types addObjectsFromArray:exttypes]; } } else { allowAll = true; [types release]; types = nil; } } } [types autorelease]; return types; } void FileDialog::DoOnFilterSelected(int index) { if (index == wxNOT_FOUND) { return; } NSArray* types = GetTypesFromExtension(m_filterExtensions[index],m_currentExtensions); NSSavePanel* panel = (NSSavePanel*) GetWXWindow(); [panel setAllowedFileTypes:types]; m_filterIndex = index; wxFileCtrlEvent event( wxEVT_FILECTRL_FILTERCHANGED, this, GetId() ); event.SetFilterIndex( m_filterIndex ); GetEventHandler()->ProcessEvent( event ); } // An item has been selected in the file filter wxChoice: void FileDialog::OnFilterSelected( wxCommandEvent &WXUNUSED(event) ) { DoOnFilterSelected( m_filterChoice->GetSelection() ); } void FileDialog::DoViewResized(void* object) { m_filterPanel->Layout(); } void FileDialog::DoSendFolderChangedEvent(void* panel, const wxString & path) { m_dir = path; wxFileCtrlEvent event( wxEVT_FILECTRL_FOLDERCHANGED, this, GetId() ); event.SetDirectory( m_dir ); GetEventHandler()->ProcessEvent( event ); } void FileDialog::DoSendSelectionChangedEvent(void* panel) { if ( HasFlag( wxFD_SAVE ) ) { NSSavePanel* sPanel = (NSSavePanel*) panel; NSString* path = [[sPanel URL] path]; wxFileName fn(wxCFStringRef::AsStringWithNormalizationFormC( path )); if (!fn.GetFullPath().empty()) { m_path = fn.GetFullPath(); m_dir = fn.GetPath(); m_fileName = fn.GetFullName(); m_fileNames.Clear(); m_fileNames.Add( m_fileName ); } } else { NSOpenPanel* oPanel = (NSOpenPanel*) panel; m_paths.Clear(); m_fileNames.Clear(); NSArray* urls = [oPanel URLs]; for ( size_t i = 0 ; i < [urls count] ; ++ i ) { NSString *path = [[urls objectAtIndex:i] path]; wxString fnstr = wxCFStringRef::AsStringWithNormalizationFormC( path ); m_paths.Add( fnstr ); m_fileNames.Add( wxFileNameFromPath( fnstr ) ); if ( i == 0 ) { m_path = fnstr; m_fileName = wxFileNameFromPath( fnstr ); m_dir = wxPathOnly( fnstr ); } } } wxFileCtrlEvent event( wxEVT_FILECTRL_SELECTIONCHANGED, this, GetId() ); event.SetDirectory( m_dir ); event.SetFiles( m_fileNames ); GetEventHandler()->ProcessEvent( event ); } void FileDialog::SetupExtraControls(WXWindow nativeWindow) { NSSavePanel* panel = (NSSavePanel*) nativeWindow; // for sandboxed app we cannot access the outer structures // this leads to problems with extra controls, so as a temporary // workaround for crashes we don't support those yet if ( [panel contentView] == nil || getenv("APP_SANDBOX_CONTAINER_ID") != NULL ) return; OSPanelDelegate* del = [[OSPanelDelegate alloc]init]; [del setFileDialog:this]; [panel setDelegate:del]; m_delegate = del; wxNonOwnedWindow::Create( GetParent(), nativeWindow ); m_filterPanel = NULL; m_filterChoice = NULL; NSView* accView = nil; if ( m_useFileTypeFilter || HasUserPaneCreator() ) { wxBoxSizer *verticalSizer = new wxBoxSizer( wxVERTICAL ); m_filterPanel = new wxPanel( this, wxID_ANY ); accView = m_filterPanel->GetHandle(); NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:del selector:@selector(viewResized:) name:NSViewFrameDidChangeNotification object:accView]; if ( m_useFileTypeFilter ) { wxBoxSizer *horizontalSizer = new wxBoxSizer( wxHORIZONTAL ); wxStaticText *stattext = new wxStaticText( m_filterPanel, wxID_ANY, XO("File type:") .Translation()); horizontalSizer->Add( stattext, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); m_filterChoice = new wxChoice( m_filterPanel, wxID_ANY ); m_filterChoice->Append( m_filterNames ); if ( m_filterNames.GetCount() > 0 ) { if ( m_firstFileTypeFilter >= 0 ) m_filterChoice->SetSelection( m_firstFileTypeFilter ); } m_filterChoice->Bind(wxEVT_CHOICE, &FileDialog::OnFilterSelected, this); horizontalSizer->Add( m_filterChoice, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); verticalSizer->Add( horizontalSizer, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); } if ( HasUserPaneCreator() ) { wxPanel *userpane = new wxPanel( m_filterPanel, wxID_ANY ); CreateUserPane( userpane ); wxBoxSizer *horizontalSizer = new wxBoxSizer( wxHORIZONTAL ); horizontalSizer->Add( userpane, 1, wxEXPAND, 0 ); verticalSizer->Add( horizontalSizer, 1, wxEXPAND, 0 ); } m_filterPanel->SetSizer( verticalSizer ); m_filterPanel->Layout(); wxSize ws = m_filterPanel->GetBestSize(); m_filterPanel->SetSize(ws); m_filterPanel->SetMinSize(ws); } if ( accView != nil ) { [accView removeFromSuperview]; [accView setAutoresizingMask:NSViewWidthSizable]; [panel setAccessoryView:accView]; } } int FileDialog::ShowModal() { WX_HOOK_MODAL_DIALOG(); wxCFEventLoopPauseIdleEvents pause; wxMacAutoreleasePool autoreleasepool; wxCFStringRef cf( m_message ); wxCFStringRef dir( m_dir ); wxCFStringRef file( m_fileName ); m_path.clear(); m_fileNames.Clear(); m_paths.Clear(); wxNonOwnedWindow* parentWindow = NULL; int returnCode = -1; if (GetParent()) { parentWindow = dynamic_cast(wxGetTopLevelParent(GetParent())); } NSArray* types = GetTypesFromFilter( m_wildCard, m_filterNames, m_filterExtensions ) ; m_useFileTypeFilter = m_filterExtensions.GetCount() > 0; #if defined(we_always_want_the_types) if( HasFlag(wxFD_OPEN) ) { if ( !(wxSystemOptions::HasOption( wxOSX_FILEDIALOG_ALWAYS_SHOW_TYPES ) && (wxSystemOptions::GetOptionInt( wxOSX_FILEDIALOG_ALWAYS_SHOW_TYPES ) == 1)) ) m_useFileTypeFilter = false; } #endif m_firstFileTypeFilter = wxNOT_FOUND; if ( m_useFileTypeFilter && m_filterIndex >= 0 && m_filterIndex < m_filterExtensions.GetCount() ) { m_firstFileTypeFilter = m_filterIndex; } else if ( m_useFileTypeFilter ) { types = nil; bool useDefault = true; for ( size_t i = 0; i < m_filterExtensions.GetCount(); ++i ) { types = GetTypesFromExtension(m_filterExtensions[i], m_currentExtensions); if ( m_currentExtensions.GetCount() == 0 ) { useDefault = false; m_firstFileTypeFilter = i; break; } for ( size_t j = 0; j < m_currentExtensions.GetCount(); ++j ) { if ( m_fileName.EndsWith(m_currentExtensions[j]) ) { m_firstFileTypeFilter = i; useDefault = false; break; } } if ( !useDefault ) break; } if ( useDefault ) { types = GetTypesFromExtension(m_filterExtensions[0], m_currentExtensions); m_firstFileTypeFilter = 0; } } OSXBeginModalDialog(); if ( HasFlag(wxFD_SAVE) ) { NSSavePanel* sPanel = [NSSavePanel savePanel]; SetupExtraControls(sPanel); // PRL: // Hack for bug 1300: intercept key down events, implement a // Command+V handler, but it's a bit crude. It always pastes // the entire text field, ignoring the insertion cursor, and ignoring // which control really has the focus. id handler; if (wxTheClipboard->IsSupported(wxDF_UNICODETEXT)) { handler = [ NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^NSEvent *(NSEvent *event) { if ([event modifierFlags] & NSCommandKeyMask) { auto chars = [event charactersIgnoringModifiers]; auto character = [chars characterAtIndex:0]; if (character == 'v') { if (wxTheClipboard->Open()) { wxTextDataObject data; wxTheClipboard->GetData(data); wxTheClipboard->Close(); wxString text = data.GetText(); auto rawText = text.utf8_str(); auto length = text.Length(); NSString *myString = [[NSString alloc] initWithBytes:rawText.data() length: rawText.length() encoding: NSUTF8StringEncoding ]; [sPanel setNameFieldStringValue:myString]; [myString release]; return nil; } } } return event; } ]; } // makes things more convenient: [sPanel setCanCreateDirectories:YES]; [sPanel setMessage:cf.AsNSString()]; // if we should be able to descend into packages we must somehow // be able to pass this in [sPanel setTreatsFilePackagesAsDirectories:NO]; [sPanel setCanSelectHiddenExtension:YES]; [sPanel setExtensionHidden:NO]; [sPanel setAllowedFileTypes:types]; [sPanel setAllowsOtherFileTypes:YES]; if ( HasFlag(wxFD_OVERWRITE_PROMPT) ) { } /* Let the file dialog know what file type should be used initially. If this is not done then when setting the filter index programmatically to 1 the file will still have the extension of the first file type instead of the second one. E.g. when file types are foo and bar, a filename "myletter" with SetDialogIndex(1) would result in saving as myletter.foo, while we want myletter.bar. */ // if(m_firstFileTypeFilter > 0) { DoOnFilterSelected(m_firstFileTypeFilter); } [sPanel setDirectoryURL:[NSURL fileURLWithPath:dir.AsNSString()]]; [sPanel setNameFieldStringValue:file.AsNSString()]; returnCode = [sPanel runModal]; ModalFinishedCallback(sPanel, returnCode); if (wxTheClipboard->IsSupported(wxDF_UNICODETEXT)) [NSEvent removeMonitor:handler]; } else { NSOpenPanel* oPanel = [NSOpenPanel openPanel]; SetupExtraControls(oPanel); [oPanel setTreatsFilePackagesAsDirectories:NO]; [oPanel setCanChooseDirectories:NO]; [oPanel setResolvesAliases:YES]; [oPanel setCanChooseFiles:YES]; [oPanel setMessage:cf.AsNSString()]; [oPanel setAllowsMultipleSelection: (HasFlag(wxFD_MULTIPLE) ? YES : NO )]; // Note that the test here is intentionally different from the one // above, in the wxFD_SAVE case: we need to call DoOnFilterSelected() // even for m_firstFileTypeFilter == 0, i.e. when using the default // filter. if ( m_firstFileTypeFilter >= 0 ) { DoOnFilterSelected(m_firstFileTypeFilter); } else { [oPanel setAllowedFileTypes: (m_delegate == nil ? types : nil)]; } if ( !m_dir.IsEmpty() ) [oPanel setDirectoryURL:[NSURL fileURLWithPath:dir.AsNSString() isDirectory:YES]]; { DoOnFilterSelected(m_firstFileTypeFilter); } returnCode = [oPanel runModal]; ModalFinishedCallback(oPanel, returnCode); } OSXEndModalDialog(); return GetReturnCode(); } void FileDialog::ModalFinishedCallback(void* panel, int returnCode) { m_paths.Clear(); m_fileNames.Clear(); int result = wxID_CANCEL; if (HasFlag(wxFD_SAVE)) { NSSavePanel* sPanel = (NSSavePanel*)panel; if (returnCode == NSOKButton ) { result = wxID_OK; NSString* path = [[sPanel URL] path]; wxFileName fn(wxCFStringRef::AsStringWithNormalizationFormC( path )); m_dir = fn.GetPath(); m_fileName = fn.GetFullName(); m_path = fn.GetFullPath(); if (m_filterChoice) { m_filterIndex = m_filterChoice->GetSelection(); } } [sPanel setDelegate:nil]; } else { NSOpenPanel* oPanel = (NSOpenPanel*)panel; if (returnCode == NSOKButton ) { panel = oPanel; result = wxID_OK; if (m_filterChoice) { m_filterIndex = m_filterChoice->GetSelection(); } NSArray* filenames = [oPanel URLs]; for ( size_t i = 0 ; i < [filenames count] ; ++ i ) { wxString fnstr = wxCFStringRef::AsStringWithNormalizationFormC([[filenames objectAtIndex:i] path]); m_paths.Add( fnstr ); m_fileNames.Add( wxFileNameFromPath(fnstr) ); if ( i == 0 ) { m_path = fnstr; m_fileName = wxFileNameFromPath(fnstr); m_dir = wxPathOnly( fnstr ); } } } [oPanel setDelegate:nil]; } if ( m_delegate ) { [[NSNotificationCenter defaultCenter] removeObserver:m_delegate]; [m_delegate release]; m_delegate = nil; } SetReturnCode(result); if (GetModality() == wxDIALOG_MODALITY_WINDOW_MODAL) SendWindowModalDialogEvent ( wxEVT_WINDOW_MODAL_DIALOG_CLOSED ); // workaround for sandboxed app, see above if ( m_isNativeWindowWrapper ) UnsubclassWin(); [(NSSavePanel*) panel setAccessoryView:nil]; } // Change the currently displayed extension void FileDialog::SetFileExtension(const wxString& extension) { NSSavePanel* sPanel = (NSSavePanel*) GetWXWindow(); m_filterExtensions[m_filterIndex] = extension; NSArray* types = GetTypesFromExtension(m_filterExtensions[m_filterIndex],m_currentExtensions); [sPanel setAllowedFileTypes:types]; }