diff options
Diffstat (limited to 'src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm')
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm | 910 |
1 files changed, 459 insertions, 451 deletions
diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm index bdf185d275..41170b74ea 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm @@ -1,219 +1,193 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtGui module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#include <AppKit/AppKit.h> +#include <QtCore/qglobal.h> -#include <qpa/qplatformtheme.h> +#include <AppKit/AppKit.h> #include "qcocoafiledialoghelper.h" - -/***************************************************************************** - QFileDialog debug facilities - *****************************************************************************/ -//#define DEBUG_FILEDIALOG_FILTERS - -#include <qguiapplication.h> -#include <private/qguiapplication_p.h> #include "qcocoahelpers.h" #include "qcocoaeventdispatcher.h" -#include <qbuffer.h> -#include <qdebug.h> -#include <qstringlist.h> -#include <qvarlengtharray.h> -#include <stdlib.h> -#include <qabstracteventdispatcher.h> -#include <qsysinfo.h> -#include <qoperatingsystemversion.h> -#include <qglobal.h> -#include <qdir.h> -#include <qregularexpression.h> +#include <QtCore/qbuffer.h> +#include <QtCore/qdebug.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qvarlengtharray.h> +#include <QtCore/qabstracteventdispatcher.h> +#include <QtCore/qdir.h> +#include <QtCore/qregularexpression.h> +#include <QtCore/qpointer.h> +#include <QtCore/private/qcore_mac_p.h> + +#include <QtGui/qguiapplication.h> +#include <QtGui/private/qguiapplication_p.h> + +#include <qpa/qplatformtheme.h> #include <qpa/qplatformnativeinterface.h> -#include <CoreFoundation/CFNumber.h> +#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> -QT_FORWARD_DECLARE_CLASS(QFileInfo) -QT_FORWARD_DECLARE_CLASS(QWindow) QT_USE_NAMESPACE +using namespace Qt::StringLiterals; + +static NSString *strippedText(QString s) +{ + s.remove("..."_L1); + return QPlatformTheme::removeMnemonics(s).trimmed().toNSString(); +} + +// NSOpenPanel extends NSSavePanel with some extra APIs +static NSOpenPanel *openpanel_cast(NSSavePanel *panel) +{ + if ([panel isKindOfClass:NSOpenPanel.class]) + return static_cast<NSOpenPanel*>(panel); + else + return nil; +} + typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; @implementation QNSOpenSavePanelDelegate { - @public - NSOpenPanel *mOpenPanel; - NSSavePanel *mSavePanel; - NSView *mAccessoryView; - NSPopUpButton *mPopUpButton; - NSTextField *mTextField; - QCocoaFileDialogHelper *mHelper; - NSString *mCurrentDir; + @public + NSSavePanel *m_panel; + NSView *m_accessoryView; + NSPopUpButton *m_popupButton; + NSTextField *m_textField; + QPointer<QCocoaFileDialogHelper> m_helper; - int mReturnCode; - - SharedPointerFileDialogOptions mOptions; - QString *mCurrentSelection; - QStringList *mNameFilterDropDownList; - QStringList *mSelectedNameFilter; + SharedPointerFileDialogOptions m_options; + QString m_currentSelection; + QStringList m_nameFilterDropDownList; + QStringList m_selectedNameFilter; } - (instancetype)initWithAcceptMode:(const QString &)selectFile options:(SharedPointerFileDialogOptions)options helper:(QCocoaFileDialogHelper *)helper { - self = [super init]; - mOptions = options; - if (mOptions->acceptMode() == QFileDialogOptions::AcceptOpen){ - mOpenPanel = [NSOpenPanel openPanel]; - mSavePanel = mOpenPanel; - } else { - mSavePanel = [NSSavePanel savePanel]; - [mSavePanel setCanSelectHiddenExtension:YES]; - mOpenPanel = nil; - } + if ((self = [super init])) { + m_options = options; + + if (m_options->acceptMode() == QFileDialogOptions::AcceptOpen) + m_panel = [[NSOpenPanel openPanel] retain]; + else + m_panel = [[NSSavePanel savePanel] retain]; + + m_panel.canSelectHiddenExtension = YES; + m_panel.level = NSModalPanelWindowLevel; + + m_helper = helper; + + m_nameFilterDropDownList = m_options->nameFilters(); + QString selectedVisualNameFilter = m_options->initiallySelectedNameFilter(); + m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selectedVisualNameFilter]; + + m_panel.extensionHidden = [&]{ + for (const auto &nameFilter : m_nameFilterDropDownList) { + const auto extensions = QPlatformFileDialogHelper::cleanFilterList(nameFilter); + for (const auto &extension : extensions) { + // Explicitly show extensions if we detect a filter + // of "all files", as clicking a single file with + // extensions hidden will then populate the name + // field with only the file name, without any + // extension. + if (extension == "*"_L1 || extension == "*.*"_L1) + return false; + + // Explicitly show extensions if we detect a filter + // that has a multi-part extension. This prevents + // confusing situations where the user clicks e.g. + // 'foo.tar.gz' and 'foo.tar' is populated in the + // file name box, but when then clicking save macOS + // will warn that the file needs to end in .gz, + // due to thinking the user tried to save the file + // as a 'tar' file instead. Unfortunately this + // property can only be set before the panel is + // shown, so we can't toggle it on and off based + // on the active filter. + if (extension.count('.') > 1) + return false; + } + } + return true; + }(); + + const QFileInfo sel(selectFile); + if (sel.isDir() && !sel.isBundle()){ + m_panel.directoryURL = [NSURL fileURLWithPath:sel.absoluteFilePath().toNSString()]; + m_currentSelection.clear(); + } else { + m_panel.directoryURL = [NSURL fileURLWithPath:sel.absolutePath().toNSString()]; + m_currentSelection = sel.absoluteFilePath(); + } - if ([mSavePanel respondsToSelector:@selector(setLevel:)]) - [mSavePanel setLevel:NSModalPanelWindowLevel]; + [self createPopUpButton:selectedVisualNameFilter hideDetails:options->testOption(QFileDialogOptions::HideNameFilterDetails)]; + [self createTextField]; + [self createAccessory]; - mReturnCode = -1; - mHelper = helper; - mNameFilterDropDownList = new QStringList(mOptions->nameFilters()); - QString selectedVisualNameFilter = mOptions->initiallySelectedNameFilter(); - mSelectedNameFilter = new QStringList([self findStrippedFilterWithVisualFilterName:selectedVisualNameFilter]); + m_panel.accessoryView = m_nameFilterDropDownList.size() > 1 ? m_accessoryView : nil; + // -setAccessoryView: can result in -panel:directoryDidChange: + // resetting our current directory. Set the delegate + // here to make sure it gets the correct value. + m_panel.delegate = self; - QFileInfo sel(selectFile); - if (sel.isDir() && !sel.isBundle()){ - mCurrentDir = [sel.absoluteFilePath().toNSString() retain]; - mCurrentSelection = new QString; - } else { - mCurrentDir = [sel.absolutePath().toNSString() retain]; - mCurrentSelection = new QString(sel.absoluteFilePath()); - } - - [mSavePanel setTitle:options->windowTitle().toNSString()]; - [self createPopUpButton:selectedVisualNameFilter hideDetails:options->testOption(QFileDialogOptions::HideNameFilterDetails)]; - [self createTextField]; - [self createAccessory]; - [mSavePanel setAccessoryView:mNameFilterDropDownList->size() > 1 ? mAccessoryView : nil]; - // -setAccessoryView: can result in -panel:directoryDidChange: - // resetting our mCurrentDir, set the delegate - // here to make sure it gets the correct value. - [mSavePanel setDelegate:self]; - mOpenPanel.accessoryViewDisclosed = YES; - - if (mOptions->isLabelExplicitlySet(QFileDialogOptions::Accept)) - [mSavePanel setPrompt:[self strip:options->labelText(QFileDialogOptions::Accept)]]; - if (mOptions->isLabelExplicitlySet(QFileDialogOptions::FileName)) - [mSavePanel setNameFieldLabel:[self strip:options->labelText(QFileDialogOptions::FileName)]]; + if (auto *openPanel = openpanel_cast(m_panel)) + openPanel.accessoryViewDisclosed = YES; - [self updateProperties]; - [mSavePanel retain]; + [self updateProperties]; + } return self; } - (void)dealloc { - delete mNameFilterDropDownList; - delete mSelectedNameFilter; - delete mCurrentSelection; - - if ([mSavePanel respondsToSelector:@selector(orderOut:)]) - [mSavePanel orderOut:mSavePanel]; - [mSavePanel setAccessoryView:nil]; - [mPopUpButton release]; - [mTextField release]; - [mAccessoryView release]; - [mSavePanel setDelegate:nil]; - [mSavePanel release]; - [mCurrentDir release]; + [m_panel orderOut:m_panel]; + m_panel.accessoryView = nil; + [m_popupButton release]; + [m_textField release]; + [m_accessoryView release]; + m_panel.delegate = nil; + [m_panel release]; [super dealloc]; } -static QString strippedText(QString s) +- (bool)showPanel:(Qt::WindowModality) windowModality withParent:(QWindow *)parent { - s.remove(QLatin1String("...")); - return QPlatformTheme::removeMnemonics(s).trimmed(); -} - -- (NSString *)strip:(const QString &)label -{ - return strippedText(label).toNSString(); -} + const QFileInfo info(m_currentSelection); + NSString *filepath = info.filePath().toNSString(); + NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()]; + bool selectable = (m_options->acceptMode() == QFileDialogOptions::AcceptSave) + || [self panel:m_panel shouldEnableURL:url]; -- (void)closePanel -{ - *mCurrentSelection = QString::fromNSString([[mSavePanel URL] path]).normalized(QString::NormalizationForm_C); - if ([mSavePanel respondsToSelector:@selector(close)]) - [mSavePanel close]; - if ([mSavePanel isSheet]) - [NSApp endSheet: mSavePanel]; -} + m_panel.nameFieldStringValue = selectable ? info.fileName().toNSString() : @""; -- (void)showModelessPanel -{ - if (mOpenPanel){ - QFileInfo info(*mCurrentSelection); - NSString *filepath = info.filePath().toNSString(); - NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()]; - bool selectable = (mOptions->acceptMode() == QFileDialogOptions::AcceptSave) - || [self panel:mOpenPanel shouldEnableURL:url]; + [self updateProperties]; - [self updateProperties]; - [mSavePanel setNameFieldStringValue:selectable ? info.fileName().toNSString() : @""]; + auto completionHandler = ^(NSInteger result) { m_helper->panelClosed(result); }; - [mOpenPanel beginWithCompletionHandler:^(NSInteger result){ - mReturnCode = result; - if (mHelper) - mHelper->QNSOpenSavePanelDelegate_panelClosed(result == NSModalResponseOK); - }]; + if (windowModality == Qt::WindowModal && parent) { + NSView *view = reinterpret_cast<NSView*>(parent->winId()); + [m_panel beginSheetModalForWindow:view.window completionHandler:completionHandler]; + } else if (windowModality == Qt::ApplicationModal) { + return true; // Defer until exec() + } else { + [m_panel beginWithCompletionHandler:completionHandler]; } + + return true; } -- (BOOL)runApplicationModalPanel +-(void)runApplicationModalPanel { - QFileInfo info(*mCurrentSelection); - NSString *filepath = info.filePath().toNSString(); - NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()]; - bool selectable = (mOptions->acceptMode() == QFileDialogOptions::AcceptSave) - || [self panel:mSavePanel shouldEnableURL:url]; + // Note: If NSApp is not running (which is the case if e.g a top-most + // QEventLoop has been interrupted, and the second-most event loop has not + // yet been reactivated (regardless if [NSApp run] is still on the stack)), + // showing a native modal dialog will fail. + if (!m_helper) + return; - [mSavePanel setDirectoryURL: [NSURL fileURLWithPath:mCurrentDir]]; - [mSavePanel setNameFieldStringValue:selectable ? info.fileName().toNSString() : @""]; + QMacAutoReleasePool pool; // Call processEvents in case the event dispatcher has been interrupted, and needs to do // cleanup of modal sessions. Do this before showing the native dialog, otherwise it will @@ -223,219 +197,299 @@ static QString strippedText(QString s) // Make sure we don't interrupt the runModal call below. QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); - mReturnCode = [mSavePanel runModal]; - - QAbstractEventDispatcher::instance()->interrupt(); - return (mReturnCode == NSModalResponseOK); -} + auto result = [m_panel runModal]; + m_helper->panelClosed(result); -- (QPlatformDialogHelper::DialogCode)dialogResultCode -{ - return (mReturnCode == NSModalResponseOK) ? QPlatformDialogHelper::Accepted : QPlatformDialogHelper::Rejected; + // Wake up the event dispatcher so it can check whether the + // current event loop should continue spinning or not. + QCoreApplication::eventDispatcher()->wakeUp(); } -- (void)showWindowModalSheet:(QWindow *)parent +- (void)closePanel { - QFileInfo info(*mCurrentSelection); - NSString *filepath = info.filePath().toNSString(); - NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()]; - bool selectable = (mOptions->acceptMode() == QFileDialogOptions::AcceptSave) - || [self panel:mSavePanel shouldEnableURL:url]; - - [self updateProperties]; - [mSavePanel setDirectoryURL: [NSURL fileURLWithPath:mCurrentDir]]; - - [mSavePanel setNameFieldStringValue:selectable ? info.fileName().toNSString() : @""]; - NSWindow *nsparent = static_cast<NSWindow *>(qGuiApp->platformNativeInterface()->nativeResourceForWindow("nswindow", parent)); + m_currentSelection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); - [mSavePanel beginSheetModalForWindow:nsparent completionHandler:^(NSInteger result){ - mReturnCode = result; - if (mHelper) - mHelper->QNSOpenSavePanelDelegate_panelClosed(result == NSModalResponseOK); - }]; -} - -- (BOOL)isHiddenFileAtURL:(NSURL *)url -{ - BOOL hidden = NO; - if (url) { - CFBooleanRef isHiddenProperty; - if (CFURLCopyResourcePropertyForKey((__bridge CFURLRef)url, kCFURLIsHiddenKey, &isHiddenProperty, nullptr)) { - hidden = CFBooleanGetValue(isHiddenProperty); - CFRelease(isHiddenProperty); - } - } - return hidden; + if (m_panel.sheet) + [NSApp endSheet:m_panel]; + else if (NSApp.modalWindow == m_panel) + [NSApp stopModal]; + else + [m_panel close]; } - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url { Q_UNUSED(sender); - NSString *filename = [url path]; - if ([filename length] == 0) + NSString *filename = url.path; + if (!filename.length) return NO; - // Always accept directories regardless of their names (unless it is a bundle): - NSFileManager *fm = [NSFileManager defaultManager]; - NSDictionary *fileAttrs = [fm attributesOfItemAtPath:filename error:nil]; - if (!fileAttrs) - return NO; // Error accessing the file means 'no'. - NSString *fileType = [fileAttrs fileType]; - bool isDir = [fileType isEqualToString:NSFileTypeDirectory]; - if (isDir) { - if ([mSavePanel treatsFilePackagesAsDirectories] == NO) { - if ([[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename] == NO) - return YES; - } + const QFileInfo fileInfo(QString::fromNSString(filename)); + + // Always accept directories regardless of their names. + // This also includes symlinks and aliases to directories. + if (fileInfo.isDir()) { + // Unless it's a bundle, and we should treat bundles as files. + // FIXME: We'd like to use QFileInfo::isBundle() here, but the + // detection in QFileInfo goes deeper than NSWorkspace does + // (likely a bug), and as a result causes TCC permission + // dialogs to pop up when used. + bool treatBundlesAsFiles = !m_panel.treatsFilePackagesAsDirectories; + if (!(treatBundlesAsFiles && [NSWorkspace.sharedWorkspace isFilePackageAtPath:filename])) + return YES; } - QString qtFileName = QFileInfo(QString::fromNSString(filename)).fileName(); - // No filter means accept everything - bool nameMatches = mSelectedNameFilter->isEmpty(); - // Check if the current file name filter accepts the file: - for (int i = 0; !nameMatches && i < mSelectedNameFilter->size(); ++i) { - if (QDir::match(mSelectedNameFilter->at(i), qtFileName)) - nameMatches = true; - } - if (!nameMatches) + if (![self fileInfoMatchesCurrentNameFilter:fileInfo]) return NO; - QDir::Filters filter = mOptions->filter(); - if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && isDir) - || (!(filter & QDir::Files) && [fileType isEqualToString:NSFileTypeRegular]) - || ((filter & QDir::NoSymLinks) && [fileType isEqualToString:NSFileTypeSymbolicLink])) + QDir::Filters filter = m_options->filter(); + if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && fileInfo.isDir()) + || (!(filter & QDir::Files) && (fileInfo.isFile() && !fileInfo.isSymLink())) + || ((filter & QDir::NoSymLinks) && fileInfo.isSymLink())) return NO; bool filterPermissions = ((filter & QDir::PermissionMask) && (filter & QDir::PermissionMask) != QDir::PermissionMask); if (filterPermissions) { - if ((!(filter & QDir::Readable) && [fm isReadableFileAtPath:filename]) - || (!(filter & QDir::Writable) && [fm isWritableFileAtPath:filename]) - || (!(filter & QDir::Executable) && [fm isExecutableFileAtPath:filename])) + if ((!(filter & QDir::Readable) && fileInfo.isReadable()) + || (!(filter & QDir::Writable) && fileInfo.isWritable()) + || (!(filter & QDir::Executable) && fileInfo.isExecutable())) return NO; } - if (!(filter & QDir::Hidden) - && (qtFileName.startsWith(QLatin1Char('.')) || [self isHiddenFileAtURL:url])) + + // We control the visibility of hidden files via the showsHiddenFiles + // property on the panel, based on QDir::Hidden being set. But the user + // can also toggle this via the Command+Shift+. keyboard shortcut, + // in which case they have explicitly requested to show hidden files, + // and we should enable them even if QDir::Hidden was not set. In + // effect, we don't need to filter on QDir::Hidden here. + + return YES; +} + +- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError * _Nullable *)outError +{ + Q_ASSERT(sender == m_panel); + + if (!m_panel.allowedFileTypes && !m_selectedNameFilter.isEmpty()) { + // The save panel hasn't done filtering on our behalf, + // either because we couldn't represent the filter via + // allowedFileTypes, or we opted out due to a multi part + // extension, so do the filtering/validation ourselves. + QFileInfo fileInfo(QString::fromNSString(url.path).normalized(QString::NormalizationForm_C)); + + if ([self fileInfoMatchesCurrentNameFilter:fileInfo]) + return YES; + + if (fileInfo.suffix().isEmpty()) { + // The filter requires a file name with an extension. + // We're going to add a default file name in selectedFiles, + // to match the native behavior. Check now that we can + // overwrite the file, if is already exists. + fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo]; + + if (!fileInfo.exists() || m_options->testOption(QFileDialogOptions::DontConfirmOverwrite)) + return YES; + + QMacAutoReleasePool pool; + auto *alert = [[NSAlert new] autorelease]; + alert.alertStyle = NSAlertStyleCritical; + + alert.messageText = [NSString stringWithFormat:qt_mac_AppKitString(@"SavePanel", + @"\\U201c%@\\U201d already exists. Do you want to replace it?"), + fileInfo.fileName().toNSString()]; + alert.informativeText = [NSString stringWithFormat:qt_mac_AppKitString(@"SavePanel", + @"A file or folder with the same name already exists in the folder %@. " + "Replacing it will overwrite its current contents."), + fileInfo.absoluteDir().dirName().toNSString()]; + + auto *replaceButton = [alert addButtonWithTitle:qt_mac_AppKitString(@"SavePanel", @"Replace")]; + replaceButton.hasDestructiveAction = YES; + replaceButton.tag = 1337; + [alert addButtonWithTitle:qt_mac_AppKitString(@"Common", @"Cancel")]; + + [alert beginSheetModalForWindow:m_panel + completionHandler:^(NSModalResponse returnCode) { + [NSApp stopModalWithCode:returnCode]; + }]; + return [NSApp runModalForWindow:alert.window] == replaceButton.tag; + } else { + QFileInfo firstFilter(m_selectedNameFilter.first()); + auto *domain = qGuiApp->organizationDomain().toNSString(); + *outError = [NSError errorWithDomain:domain code:0 userInfo:@{ + NSLocalizedDescriptionKey:[NSString stringWithFormat:qt_mac_AppKitString(@"SavePanel", + @"You cannot save this document with extension \\U201c.%1$@\\U201d at the end " + "of the name. The required extension is \\U201c.%2$@\\U201d."), + fileInfo.completeSuffix().toNSString(), firstFilter.completeSuffix().toNSString()] + }]; return NO; + } + } return YES; } -- (NSString *)panel:(id)sender userEnteredFilename:(NSString *)filename confirmed:(BOOL)okFlag +- (QFileInfo)applyDefaultSuffixFromCurrentNameFilter:(const QFileInfo &)fileInfo { - Q_UNUSED(sender); - if (!okFlag) - return filename; - if (!mOptions->testOption(QFileDialogOptions::DontConfirmOverwrite)) - return filename; + QFileInfo filterInfo(m_selectedNameFilter.first()); + return QFileInfo(fileInfo.absolutePath(), + fileInfo.baseName() + '.' + filterInfo.completeSuffix()); +} - // User has clicked save, and no overwrite confirmation should occur. - // To get the latter, we need to change the name we return (hence the prefix): - return [@"___qt_very_unlikely_prefix_" stringByAppendingString:filename]; +- (bool)fileInfoMatchesCurrentNameFilter:(const QFileInfo &)fileInfo +{ + // No filter means accept everything + if (m_selectedNameFilter.isEmpty()) + return true; + + // Check if the current file name filter accepts the file + for (const auto &filter : m_selectedNameFilter) { + if (QDir::match(filter, fileInfo.fileName())) + return true; + } + + return false; } - (void)setNameFilters:(const QStringList &)filters hideDetails:(BOOL)hideDetails { - [mPopUpButton removeAllItems]; - *mNameFilterDropDownList = filters; + [m_popupButton removeAllItems]; + m_nameFilterDropDownList = filters; if (filters.size() > 0){ - for (int i=0; i<filters.size(); ++i) { - QString filter = hideDetails ? [self removeExtensions:filters.at(i)] : filters.at(i); - [mPopUpButton addItemWithTitle:filter.toNSString()]; + for (int i = 0; i < filters.size(); ++i) { + const QString filter = hideDetails ? [self removeExtensions:filters.at(i)] : filters.at(i); + [m_popupButton.menu addItemWithTitle:filter.toNSString() action:nil keyEquivalent:@""]; } - [mPopUpButton selectItemAtIndex:0]; - [mSavePanel setAccessoryView:mAccessoryView]; - } else - [mSavePanel setAccessoryView:nil]; + [m_popupButton selectItemAtIndex:0]; + m_panel.accessoryView = m_accessoryView; + } else { + m_panel.accessoryView = nil; + } [self filterChanged:self]; } - (void)filterChanged:(id)sender { - // This mDelegate function is called when the _name_ filter changes. + // This m_delegate function is called when the _name_ filter changes. Q_UNUSED(sender); - QString selection = mNameFilterDropDownList->value([mPopUpButton indexOfSelectedItem]); - *mSelectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection]; - if ([mSavePanel respondsToSelector:@selector(validateVisibleColumns:)]) - [mSavePanel validateVisibleColumns]; + if (!m_helper) + return; + const QString selection = m_nameFilterDropDownList.value([m_popupButton indexOfSelectedItem]); + m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection]; + [m_panel validateVisibleColumns]; [self updateProperties]; - if (mHelper) - mHelper->QNSOpenSavePanelDelegate_filterSelected([mPopUpButton indexOfSelectedItem]); -} -- (QString)currentNameFilter -{ - return mNameFilterDropDownList->value([mPopUpButton indexOfSelectedItem]); + const QStringList filters = m_options->nameFilters(); + const int menuIndex = m_popupButton.indexOfSelectedItem; + emit m_helper->filterSelected(menuIndex >= 0 && menuIndex < filters.size() ? filters.at(menuIndex) : QString()); } - (QList<QUrl>)selectedFiles { - if (mOpenPanel) { + if (auto *openPanel = openpanel_cast(m_panel)) { QList<QUrl> result; - NSArray<NSURL *> *array = [mOpenPanel URLs]; - for (NSURL *url in array) { + for (NSURL *url in openPanel.URLs) { QString path = QString::fromNSString(url.path).normalized(QString::NormalizationForm_C); result << QUrl::fromLocalFile(path); } return result; } else { - QList<QUrl> result; - QString filename = QString::fromNSString([[mSavePanel URL] path]).normalized(QString::NormalizationForm_C); - const QString defaultSuffix = mOptions->defaultSuffix(); - const QFileInfo fileInfo(filename); + QString filename = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); + QFileInfo fileInfo(filename); + + if (fileInfo.suffix().isEmpty() && ![self fileInfoMatchesCurrentNameFilter:fileInfo]) { + // We end up in this situation if we accept a file name without extension + // in panel:validateURL:error. If so, we match the behavior of the native + // save dialog and add the first of the accepted extension from the filter. + fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo]; + } + // If neither the user or the NSSavePanel have provided a suffix, use // the default suffix (if it exists). + const QString defaultSuffix = m_options->defaultSuffix(); if (fileInfo.suffix().isEmpty() && !defaultSuffix.isEmpty()) { - filename.append('.').append(defaultSuffix); + fileInfo.setFile(fileInfo.absolutePath(), + fileInfo.baseName() + '.' + defaultSuffix); } - result << QUrl::fromLocalFile(filename.remove(QLatin1String("___qt_very_unlikely_prefix_"))); - return result; + + return { QUrl::fromLocalFile(fileInfo.filePath()) }; } } - (void)updateProperties { - // Call this functions if mFileMode, mFileOptions, - // mNameFilterDropDownList or mQDirFilter changes. - // The savepanel does not contain the necessary functions for this. - const QFileDialogOptions::FileMode fileMode = mOptions->fileMode(); + const QFileDialogOptions::FileMode fileMode = m_options->fileMode(); bool chooseFilesOnly = fileMode == QFileDialogOptions::ExistingFile || fileMode == QFileDialogOptions::ExistingFiles; bool chooseDirsOnly = fileMode == QFileDialogOptions::Directory || fileMode == QFileDialogOptions::DirectoryOnly - || mOptions->testOption(QFileDialogOptions::ShowDirsOnly); - - [mOpenPanel setCanChooseFiles:!chooseDirsOnly]; - [mOpenPanel setCanChooseDirectories:!chooseFilesOnly]; - [mSavePanel setCanCreateDirectories:!(mOptions->testOption(QFileDialogOptions::ReadOnly))]; - [mOpenPanel setAllowsMultipleSelection:(fileMode == QFileDialogOptions::ExistingFiles)]; - [mOpenPanel setResolvesAliases:!(mOptions->testOption(QFileDialogOptions::DontResolveSymlinks))]; - [mOpenPanel setTitle:mOptions->windowTitle().toNSString()]; - [mSavePanel setTitle:mOptions->windowTitle().toNSString()]; - [mPopUpButton setHidden:chooseDirsOnly]; // TODO hide the whole sunken pane instead? - - if (mOptions->acceptMode() == QFileDialogOptions::AcceptSave) { - [self recomputeAcceptableExtensionsForSave]; - } else { - [mOpenPanel setAllowedFileTypes:nil]; // delegate panel:shouldEnableURL: does the file filtering for NSOpenPanel + || m_options->testOption(QFileDialogOptions::ShowDirsOnly); + + m_panel.title = m_options->windowTitle().toNSString(); + m_panel.canCreateDirectories = !(m_options->testOption(QFileDialogOptions::ReadOnly)); + + if (m_options->isLabelExplicitlySet(QFileDialogOptions::Accept)) + m_panel.prompt = strippedText(m_options->labelText(QFileDialogOptions::Accept)); + if (m_options->isLabelExplicitlySet(QFileDialogOptions::FileName)) + m_panel.nameFieldLabel = strippedText(m_options->labelText(QFileDialogOptions::FileName)); + + if (auto *openPanel = openpanel_cast(m_panel)) { + openPanel.canChooseFiles = !chooseDirsOnly; + openPanel.canChooseDirectories = !chooseFilesOnly; + openPanel.allowsMultipleSelection = (fileMode == QFileDialogOptions::ExistingFiles); + openPanel.resolvesAliases = !(m_options->testOption(QFileDialogOptions::DontResolveSymlinks)); } - if ([mSavePanel respondsToSelector:@selector(isVisible)] && [mSavePanel isVisible]) { - if ([mSavePanel respondsToSelector:@selector(validateVisibleColumns)]) - [mSavePanel validateVisibleColumns]; + m_popupButton.hidden = chooseDirsOnly; // TODO hide the whole sunken pane instead? + + m_panel.allowedFileTypes = [self computeAllowedFileTypes]; + + // Setting allowedFileTypes to nil is not enough to reset any + // automatically added extension based on a previous filter. + // This is problematic because extensions can in some cases + // be hidden from the user, resulting in confusion when the + // resulting file name doesn't match the current empty filter. + // We work around this by temporarily resetting the allowed + // content type to one without an extension, which forces + // the save panel to update and remove the extension. + const bool nameFieldHasExtension = m_panel.nameFieldStringValue.pathExtension.length > 0; + if (!m_panel.allowedFileTypes && !nameFieldHasExtension && !openpanel_cast(m_panel)) { + if (!UTTypeDirectory.preferredFilenameExtension) { + m_panel.allowedContentTypes = @[ UTTypeDirectory ]; + m_panel.allowedFileTypes = nil; + } else { + qWarning() << "UTTypeDirectory unexpectedly reported an extension"; + } } + + m_panel.showsHiddenFiles = m_options->filter().testFlag(QDir::Hidden); + + if (m_panel.visible) + [m_panel validateVisibleColumns]; } - (void)panelSelectionDidChange:(id)sender { Q_UNUSED(sender); - if (mHelper && [mSavePanel isVisible]) { - QString selection = QString::fromNSString([[mSavePanel URL] path]); - if (selection != *mCurrentSelection) { - *mCurrentSelection = selection; - mHelper->QNSOpenSavePanelDelegate_selectionChanged(selection); + + if (!m_helper) + return; + + // Save panels only allow you to select directories, which + // means currentChanged will only be emitted when selecting + // a directory, and if so, with the latest chosen file name, + // which is confusing and inconsistent. We choose to bail + // out entirely for save panels, to give consistent behavior. + if (!openpanel_cast(m_panel)) + return; + + if (m_panel.visible) { + const QString selection = QString::fromNSString(m_panel.URL.path); + if (selection != m_currentSelection) { + m_currentSelection = selection; + emit m_helper->currentChanged(QUrl::fromLocalFile(selection)); } } } @@ -443,14 +497,11 @@ static QString strippedText(QString s) - (void)panel:(id)sender directoryDidChange:(NSString *)path { Q_UNUSED(sender); - if (!mHelper) - return; - if (!(path && path.length) || [path isEqualToString:mCurrentDir]) + + if (!m_helper) return; - [mCurrentDir release]; - mCurrentDir = [path retain]; - mHelper->QNSOpenSavePanelDelegate_directoryEntered(QString::fromNSString(mCurrentDir)); + m_helper->panelDirectoryDidChange(path); } /* @@ -458,45 +509,34 @@ static QString strippedText(QString s) for the current name filter, and updates the save panel. If a filter do not conform to the format *.xyz or * or *.*, - all files types are allowed. - - Extensions with more than one part (e.g. "tar.gz") are - reduced to their final part, as NSSavePanel does not deal - well with multi-part extensions. + or contains an extensions with more than one part (e.g. "tar.gz") + we treat that as allowing all file types, and do our own + validation in panel:validateURL:error. */ -- (void)recomputeAcceptableExtensionsForSave +- (NSArray<NSString*>*)computeAllowedFileTypes { + if (m_options->acceptMode() != QFileDialogOptions::AcceptSave) + return nil; // panel:shouldEnableURL: does the file filtering for NSOpenPanel + QStringList fileTypes; - for (const QString &filter : *mSelectedNameFilter) { - if (!filter.startsWith(QLatin1String("*."))) + for (const QString &filter : std::as_const(m_selectedNameFilter)) { + if (!filter.startsWith("*."_L1)) continue; - if (filter.contains(QLatin1Char('?'))) + if (filter.contains(u'?')) continue; - if (filter.count(QLatin1Char('*')) != 1) + if (filter.count(u'*') != 1) continue; auto extensions = filter.split('.', Qt::SkipEmptyParts); - fileTypes += extensions.last(); + if (extensions.count() > 2) + return nil; - // Explicitly show extensions if we detect a filter - // that has a multi-part extension. This prevents - // confusing situations where the user clicks e.g. - // 'foo.tar.gz' and 'foo.tar' is populated in the - // file name box, but when then clicking save macOS - // will warn that the file needs to end in .gz, - // due to thinking the user tried to save the file - // as a 'tar' file instead. Unfortunately this - // property can only be set before the panel is - // shown, so it will not have any effect when - // swithcing filters in an already opened dialog. - if (extensions.size() > 2) - mSavePanel.extensionHidden = NO; + fileTypes += extensions.last(); } - mSavePanel.allowedFileTypes = fileTypes.isEmpty() ? nil - : qt_mac_QStringListToNSMutableArray(fileTypes); + return fileTypes.isEmpty() ? nil : qt_mac_QStringListToNSMutableArray(fileTypes); } - (QString)removeExtensions:(const QString &)filter @@ -511,45 +551,44 @@ static QString strippedText(QString s) - (void)createTextField { NSRect textRect = { { 0.0, 3.0 }, { 100.0, 25.0 } }; - mTextField = [[NSTextField alloc] initWithFrame:textRect]; - [[mTextField cell] setFont:[NSFont systemFontOfSize: - [NSFont systemFontSizeForControlSize:NSControlSizeRegular]]]; - [mTextField setAlignment:NSTextAlignmentRight]; - [mTextField setEditable:false]; - [mTextField setSelectable:false]; - [mTextField setBordered:false]; - [mTextField setDrawsBackground:false]; - if (mOptions->isLabelExplicitlySet(QFileDialogOptions::FileType)) - [mTextField setStringValue:[self strip:mOptions->labelText(QFileDialogOptions::FileType)]]; + m_textField = [[NSTextField alloc] initWithFrame:textRect]; + m_textField.cell.font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSControlSizeRegular]]; + m_textField.alignment = NSTextAlignmentRight; + m_textField.editable = false; + m_textField.selectable = false; + m_textField.bordered = false; + m_textField.drawsBackground = false; + if (m_options->isLabelExplicitlySet(QFileDialogOptions::FileType)) + m_textField.stringValue = strippedText(m_options->labelText(QFileDialogOptions::FileType)); } - (void)createPopUpButton:(const QString &)selectedFilter hideDetails:(BOOL)hideDetails { NSRect popUpRect = { { 100.0, 5.0 }, { 250.0, 25.0 } }; - mPopUpButton = [[NSPopUpButton alloc] initWithFrame:popUpRect pullsDown:NO]; - [mPopUpButton setTarget:self]; - [mPopUpButton setAction:@selector(filterChanged:)]; + m_popupButton = [[NSPopUpButton alloc] initWithFrame:popUpRect pullsDown:NO]; + m_popupButton.target = self; + m_popupButton.action = @selector(filterChanged:); - if (mNameFilterDropDownList->size() > 0) { + if (!m_nameFilterDropDownList.isEmpty()) { int filterToUse = -1; - for (int i=0; i<mNameFilterDropDownList->size(); ++i) { - QString currentFilter = mNameFilterDropDownList->at(i); + for (int i = 0; i < m_nameFilterDropDownList.size(); ++i) { + const QString currentFilter = m_nameFilterDropDownList.at(i); if (selectedFilter == currentFilter || (filterToUse == -1 && currentFilter.startsWith(selectedFilter))) filterToUse = i; QString filter = hideDetails ? [self removeExtensions:currentFilter] : currentFilter; - [mPopUpButton addItemWithTitle:filter.toNSString()]; + [m_popupButton.menu addItemWithTitle:filter.toNSString() action:nil keyEquivalent:@""]; } if (filterToUse != -1) - [mPopUpButton selectItemAtIndex:filterToUse]; + [m_popupButton selectItemAtIndex:filterToUse]; } } - (QStringList) findStrippedFilterWithVisualFilterName:(QString)name { - for (int i=0; i<mNameFilterDropDownList->size(); ++i) { - if (mNameFilterDropDownList->at(i).startsWith(name)) - return QPlatformFileDialogHelper::cleanFilterList(mNameFilterDropDownList->at(i)); + for (const QString ¤tFilter : std::as_const(m_nameFilterDropDownList)) { + if (currentFilter.startsWith(name)) + return QPlatformFileDialogHelper::cleanFilterList(currentFilter); } return QStringList(); } @@ -557,9 +596,9 @@ static QString strippedText(QString s) - (void)createAccessory { NSRect accessoryRect = { { 0.0, 0.0 }, { 450.0, 33.0 } }; - mAccessoryView = [[NSView alloc] initWithFrame:accessoryRect]; - [mAccessoryView addSubview:mTextField]; - [mAccessoryView addSubview:mPopUpButton]; + m_accessoryView = [[NSView alloc] initWithFrame:accessoryRect]; + [m_accessoryView addSubview:m_textField]; + [m_accessoryView addSubview:m_popupButton]; } @end @@ -567,60 +606,53 @@ static QString strippedText(QString s) QT_BEGIN_NAMESPACE QCocoaFileDialogHelper::QCocoaFileDialogHelper() - : mDelegate(nil) { } QCocoaFileDialogHelper::~QCocoaFileDialogHelper() { - if (!mDelegate) + if (!m_delegate) return; - QMacAutoReleasePool pool; - [mDelegate release]; - mDelegate = nil; -} -void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_selectionChanged(const QString &newPath) -{ - emit currentChanged(QUrl::fromLocalFile(newPath)); + QMacAutoReleasePool pool; + [m_delegate release]; + m_delegate = nil; } -void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_panelClosed(bool accepted) +void QCocoaFileDialogHelper::panelClosed(NSInteger result) { - if (accepted) { + if (result == NSModalResponseOK) emit accept(); - } else { + else emit reject(); - } } -void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_directoryEntered(const QString &newDir) +void QCocoaFileDialogHelper::setDirectory(const QUrl &directory) { - // ### fixme: priv->setLastVisitedDirectory(newDir); - emit directoryEntered(QUrl::fromLocalFile(newDir)); -} + m_directory = directory; -void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_filterSelected(int menuIndex) -{ - const QStringList filters = options()->nameFilters(); - emit filterSelected(menuIndex >= 0 && menuIndex < filters.size() ? filters.at(menuIndex) : QString()); + if (m_delegate) + m_delegate->m_panel.directoryURL = [NSURL fileURLWithPath:directory.toLocalFile().toNSString()]; } -void QCocoaFileDialogHelper::setDirectory(const QUrl &directory) +QUrl QCocoaFileDialogHelper::directory() const { - if (mDelegate) - [mDelegate->mSavePanel setDirectoryURL:[NSURL fileURLWithPath:directory.toLocalFile().toNSString()]]; - else - mDir = directory; + return m_directory; } -QUrl QCocoaFileDialogHelper::directory() const +void QCocoaFileDialogHelper::panelDirectoryDidChange(NSString *path) { - if (mDelegate) { - QString path = QString::fromNSString([[mDelegate->mSavePanel directoryURL] path]).normalized(QString::NormalizationForm_C); - return QUrl::fromLocalFile(path); + if (!path || [path isEqual:NSNull.null] || !path.length) + return; + + const auto oldDirectory = m_directory; + m_directory = QUrl::fromLocalFile( + QString::fromNSString(path).normalized(QString::NormalizationForm_C)); + + if (m_directory != oldDirectory) { + // FIXME: Plumb old directory back to QFileDialog's lastVisitedDir? + emit directoryEntered(m_directory); } - return mDir; } void QCocoaFileDialogHelper::selectFile(const QUrl &filename) @@ -636,23 +668,17 @@ void QCocoaFileDialogHelper::selectFile(const QUrl &filename) QList<QUrl> QCocoaFileDialogHelper::selectedFiles() const { - if (mDelegate) - return [mDelegate selectedFiles]; + if (m_delegate) + return [m_delegate selectedFiles]; return QList<QUrl>(); } void QCocoaFileDialogHelper::setFilter() { - if (!mDelegate) + if (!m_delegate) return; - const SharedPointerFileDialogOptions &opts = options(); - [mDelegate->mSavePanel setTitle:opts->windowTitle().toNSString()]; - if (opts->isLabelExplicitlySet(QFileDialogOptions::Accept)) - [mDelegate->mSavePanel setPrompt:[mDelegate strip:opts->labelText(QFileDialogOptions::Accept)]]; - if (opts->isLabelExplicitlySet(QFileDialogOptions::FileName)) - [mDelegate->mSavePanel setNameFieldLabel:[mDelegate strip:opts->labelText(QFileDialogOptions::FileName)]]; - [mDelegate updateProperties]; + [m_delegate updateProperties]; } void QCocoaFileDialogHelper::selectNameFilter(const QString &filter) @@ -661,20 +687,20 @@ void QCocoaFileDialogHelper::selectNameFilter(const QString &filter) return; const int index = options()->nameFilters().indexOf(filter); if (index != -1) { - if (!mDelegate) { + if (!m_delegate) { options()->setInitiallySelectedNameFilter(filter); return; } - [mDelegate->mPopUpButton selectItemAtIndex:index]; - [mDelegate filterChanged:nil]; + [m_delegate->m_popupButton selectItemAtIndex:index]; + [m_delegate filterChanged:nil]; } } QString QCocoaFileDialogHelper::selectedNameFilter() const { - if (!mDelegate) + if (!m_delegate) return options()->initiallySelectedNameFilter(); - int index = [mDelegate->mPopUpButton indexOfSelectedItem]; + int index = [m_delegate->m_popupButton indexOfSelectedItem]; if (index >= options()->nameFilters().count()) return QString(); return index != -1 ? options()->nameFilters().at(index) : QString(); @@ -682,12 +708,17 @@ QString QCocoaFileDialogHelper::selectedNameFilter() const void QCocoaFileDialogHelper::hide() { - hideCocoaFilePanel(); + if (!m_delegate) + return; + + [m_delegate closePanel]; + + if (m_eventLoop) + m_eventLoop->exit(); } bool QCocoaFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) { -// Q_Q(QFileDialog); if (windowFlags & Qt::WindowStaysOnTopHint) { // The native file dialog tries all it can to stay // on the NSModalPanel level. And it might also show @@ -696,7 +727,9 @@ bool QCocoaFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModalit return false; } - return showCocoaFilePanel(windowModality, parent); + createNSOpenSavePanelDelegate(); + + return [m_delegate showPanel:windowModality withParent:parent]; } void QCocoaFileDialogHelper::createNSOpenSavePanelDelegate() @@ -705,7 +738,7 @@ void QCocoaFileDialogHelper::createNSOpenSavePanelDelegate() const SharedPointerFileDialogOptions &opts = options(); const QList<QUrl> selectedFiles = opts->initiallySelectedFiles(); - const QUrl directory = mDir.isEmpty() ? opts->initialDirectory() : mDir; + const QUrl directory = m_directory.isEmpty() ? opts->initialDirectory() : m_directory; const bool selectDir = selectedFiles.isEmpty(); QString selection(selectDir ? directory.toLocalFile() : selectedFiles.front().toLocalFile()); QNSOpenSavePanelDelegate *delegate = [[QNSOpenSavePanelDelegate alloc] @@ -714,51 +747,26 @@ void QCocoaFileDialogHelper::createNSOpenSavePanelDelegate() options:opts helper:this]; - [static_cast<QNSOpenSavePanelDelegate *>(mDelegate) release]; - mDelegate = delegate; + [static_cast<QNSOpenSavePanelDelegate *>(m_delegate) release]; + m_delegate = delegate; } -bool QCocoaFileDialogHelper::showCocoaFilePanel(Qt::WindowModality windowModality, QWindow *parent) +void QCocoaFileDialogHelper::exec() { - createNSOpenSavePanelDelegate(); - if (!mDelegate) - return false; - if (windowModality == Qt::NonModal) - [mDelegate showModelessPanel]; - else if (windowModality == Qt::WindowModal && parent) - [mDelegate showWindowModalSheet:parent]; - // no need to show a Qt::ApplicationModal dialog here, since it will be done in _q_platformRunNativeAppModalPanel() - return true; -} + Q_ASSERT(m_delegate); -bool QCocoaFileDialogHelper::hideCocoaFilePanel() -{ - if (!mDelegate){ - // Nothing to do. We return false to leave the question - // open regarding whether or not to go native: - return false; + if (m_delegate->m_panel.visible) { + // WindowModal or NonModal, so already shown above + QEventLoop eventLoop; + m_eventLoop = &eventLoop; + eventLoop.exec(QEventLoop::DialogExec); + m_eventLoop = nullptr; } else { - [mDelegate closePanel]; - // Even when we hide it, we are still using a - // native dialog, so return true: - return true; + // ApplicationModal, so show and block using native APIs + [m_delegate runApplicationModalPanel]; } } -void QCocoaFileDialogHelper::exec() -{ - // Note: If NSApp is not running (which is the case if e.g a top-most - // QEventLoop has been interrupted, and the second-most event loop has not - // yet been reactivated (regardless if [NSApp run] is still on the stack)), - // showing a native modal dialog will fail. - QMacAutoReleasePool pool; - if ([mDelegate runApplicationModalPanel]) - emit accept(); - else - emit reject(); - -} - bool QCocoaFileDialogHelper::defaultNameFilterDisables() const { return true; |