5#include <QtCore/qglobal.h>
7#include <AppKit/AppKit.h>
13#include <QtCore/qbuffer.h>
14#include <QtCore/qdebug.h>
15#include <QtCore/qstringlist.h>
16#include <QtCore/qvarlengtharray.h>
17#include <QtCore/qabstracteventdispatcher.h>
18#include <QtCore/qdir.h>
19#include <QtCore/qregularexpression.h>
20#include <QtCore/qpointer.h>
21#include <QtCore/private/qcore_mac_p.h>
22#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h>
24#include <QtGui/qguiapplication.h>
25#include <QtGui/private/qguiapplication_p.h>
27#include <qpa/qplatformtheme.h>
28#include <qpa/qplatformnativeinterface.h>
30#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
34using namespace Qt::StringLiterals;
39 return QPlatformTheme::removeMnemonics(s).trimmed().toNSString();
45 if ([panel isKindOfClass:NSOpenPanel.
class])
46 return static_cast<NSOpenPanel*>(panel);
53@implementation QNSOpenSavePanelDelegate {
56 NSView *m_accessoryView;
57 NSPopUpButton *m_popupButton;
58 NSTextField *m_textField;
59 QPointer<QCocoaFileDialogHelper> m_helper;
61 SharedPointerFileDialogOptions m_options;
62 QString m_currentSelection;
63 QStringList m_nameFilterDropDownList;
64 QStringList m_selectedNameFilter;
67- (instancetype)initWithAcceptMode:(
const QString &)selectFile
68 options:(SharedPointerFileDialogOptions)options
69 helper:(QCocoaFileDialogHelper *)helper
71 if ((self = [super init])) {
74 if (m_options->acceptMode() == QFileDialogOptions::AcceptOpen)
75 m_panel = [[NSOpenPanel openPanel] retain];
77 m_panel = [[NSSavePanel savePanel] retain];
79 m_panel.canSelectHiddenExtension = YES;
80 m_panel.level = NSModalPanelWindowLevel;
84 m_nameFilterDropDownList = m_options->nameFilters();
85 QString selectedVisualNameFilter = m_options->initiallySelectedNameFilter();
86 m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selectedVisualNameFilter];
88 m_panel.extensionHidden = [&]{
89 for (
const auto &nameFilter : m_nameFilterDropDownList) {
90 const auto extensions = QPlatformFileDialogHelper::cleanFilterList(nameFilter);
91 for (
const auto &extension : extensions) {
97 if (extension ==
"*"_L1 || extension ==
"*.*"_L1)
111 if (extension.count(
'.') > 1)
118 const QFileInfo sel(selectFile);
119 if (sel.isDir() && !sel.isBundle()){
120 m_panel.directoryURL = [NSURL fileURLWithPath:sel.absoluteFilePath().toNSString()];
121 m_currentSelection.clear();
123 m_panel.directoryURL = [NSURL fileURLWithPath:sel.absolutePath().toNSString()];
124 m_currentSelection = sel.absoluteFilePath();
127 [self createPopUpButton:selectedVisualNameFilter hideDetails:options->testOption(QFileDialogOptions::HideNameFilterDetails)];
128 [self createTextField];
129 [self createAccessory];
131 m_panel.accessoryView = m_nameFilterDropDownList.size() > 1 ? m_accessoryView : nil;
135 m_panel.delegate = self;
137 if (
auto *openPanel = openpanel_cast(m_panel))
138 openPanel.accessoryViewDisclosed = YES;
140 [self updateProperties];
147 [m_panel orderOut:m_panel];
148 m_panel.accessoryView = nil;
149 [m_popupButton release];
150 [m_textField release];
151 [m_accessoryView release];
152 m_panel.delegate = nil;
157- (
bool)showPanel:(Qt::WindowModality) windowModality withParent:(QWindow *)parent
159 const QFileInfo info(m_currentSelection);
160 NSString *filepath = info.filePath().toNSString();
161 NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()];
162 bool selectable = (m_options->acceptMode() == QFileDialogOptions::AcceptSave)
163 || [self panel:m_panel shouldEnableURL:url];
165 if (!openpanel_cast(m_panel))
166 m_panel.nameFieldStringValue = selectable ? info.fileName().toNSString() : @
"";
168 [self updateProperties];
170 auto completionHandler = ^(NSInteger result) {
172 m_helper->panelClosed(result);
175 if (windowModality == Qt::WindowModal && parent) {
176 NSView *view =
reinterpret_cast<NSView*>(parent->winId());
177 [m_panel beginSheetModalForWindow:view.window completionHandler:completionHandler];
178 }
else if (windowModality == Qt::ApplicationModal) {
181 [m_panel beginWithCompletionHandler:completionHandler];
187-(
void)runApplicationModalPanel
196 QMacAutoReleasePool pool;
201 qApp->processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers);
204 QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag();
206 auto result = [m_panel runModal];
207 m_helper->panelClosed(result);
211 QCoreApplication::eventDispatcher()->wakeUp();
216 m_currentSelection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C);
219 [NSApp endSheet:m_panel];
220 else if (NSApp.modalWindow == m_panel)
226- (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url
230 NSString *filename = url.path;
231 if (!filename.length)
234 const QFileInfo fileInfo(QString::fromNSString(filename));
238 if (fileInfo.isDir()) {
244 bool treatBundlesAsFiles = !m_panel.treatsFilePackagesAsDirectories;
245 if (!(treatBundlesAsFiles && [NSWorkspace.sharedWorkspace isFilePackageAtPath:filename]))
249 if (![self fileInfoMatchesCurrentNameFilter:fileInfo])
252 QDir::Filters filter = m_options->filter();
253 if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && fileInfo.isDir())
254 || (!(filter & QDir::Files) && (fileInfo.isFile() && !fileInfo.isSymLink()))
255 || ((filter & QDir::NoSymLinks) && fileInfo.isSymLink()))
258 bool filterPermissions = ((filter & QDir::PermissionMask)
259 && (filter & QDir::PermissionMask) != QDir::PermissionMask);
260 if (filterPermissions) {
261 if ((!(filter & QDir::Readable) && fileInfo.isReadable())
262 || (!(filter & QDir::Writable) && fileInfo.isWritable())
263 || (!(filter & QDir::Executable) && fileInfo.isExecutable()))
277- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError *
_Nullable *)outError
279 Q_ASSERT(sender == m_panel);
281 if (![m_panel.allowedContentTypes count] && !m_selectedNameFilter.isEmpty()) {
286 QFileInfo fileInfo(QString::fromNSString(url.path).normalized(QString::NormalizationForm_C));
288 if ([self fileInfoMatchesCurrentNameFilter:fileInfo])
291 if (fileInfo.suffix().isEmpty()) {
296 fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo];
298 if (!fileInfo.exists() || m_options->testOption(QFileDialogOptions::DontConfirmOverwrite))
301 QMacAutoReleasePool pool;
302 auto *alert = [[NSAlert
new] autorelease];
303 alert.alertStyle = NSAlertStyleCritical;
305 alert.messageText = [NSString stringWithFormat:qt_mac_AppKitString(@
"SavePanel",
306 @
"\\U201c%@\\U201d already exists. Do you want to replace it?"),
307 fileInfo.fileName().toNSString()];
308 alert.informativeText = [NSString stringWithFormat:qt_mac_AppKitString(@
"SavePanel",
309 @
"A file or folder with the same name already exists in the folder %@. "
310 "Replacing it will overwrite its current contents."),
311 fileInfo.absoluteDir().dirName().toNSString()];
313 auto *replaceButton = [alert addButtonWithTitle:qt_mac_AppKitString(@
"SavePanel", @
"Replace")];
314 replaceButton.hasDestructiveAction = YES;
315 replaceButton.tag = 1337;
316 [alert addButtonWithTitle:qt_mac_AppKitString(@
"Common", @
"Cancel")];
318 [alert beginSheetModalForWindow:m_panel
319 completionHandler:^(NSModalResponse returnCode) {
320 [NSApp stopModalWithCode:returnCode];
322 return [NSApp runModalForWindow:alert.window] == replaceButton.tag;
324 QFileInfo firstFilter(m_selectedNameFilter.first());
325 auto *domain = qGuiApp->organizationDomain().toNSString();
326 *outError = [NSError errorWithDomain:domain code:0 userInfo:@{
327 NSLocalizedDescriptionKey:[NSString stringWithFormat:qt_mac_AppKitString(@
"SavePanel",
328 @
"You cannot save this document with extension \\U201c.%1$@\\U201d at the end "
329 "of the name. The required extension is \\U201c.%2$@\\U201d."),
330 fileInfo.completeSuffix().toNSString(), firstFilter.completeSuffix().toNSString()]
339- (
QFileInfo)applyDefaultSuffixFromCurrentNameFilter:(
const QFileInfo &)fileInfo
341 QFileInfo filterInfo(m_selectedNameFilter.first());
342 return QFileInfo(fileInfo.absolutePath(),
343 fileInfo.baseName() +
'.' + filterInfo.completeSuffix());
346- (
bool)fileInfoMatchesCurrentNameFilter:(
const QFileInfo &)fileInfo
349 if (m_selectedNameFilter.isEmpty())
353 for (
const auto &filter : m_selectedNameFilter) {
354 if (QDir::match(filter, fileInfo.fileName()))
361- (
void)setNameFilters:(
const QStringList &)filters hideDetails:(BOOL)hideDetails
363 [m_popupButton removeAllItems];
364 m_nameFilterDropDownList = filters;
365 if (filters.size() > 0){
366 for (
int i = 0; i < filters.size(); ++i) {
367 const QString filter = hideDetails ? [self removeExtensions:filters.at(i)] : filters.at(i);
368 [m_popupButton.menu addItemWithTitle:filter.toNSString() action:nil keyEquivalent:@
""];
370 [m_popupButton selectItemAtIndex:0];
371 m_panel.accessoryView = m_accessoryView;
373 m_panel.accessoryView = nil;
376 [self filterChanged:self];
379- (
void)filterChanged:(id)sender
385 const QString selection = m_nameFilterDropDownList.value([m_popupButton indexOfSelectedItem]);
386 m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection];
387 [m_panel validateVisibleColumns];
388 [self updateProperties];
390 const QStringList filters = m_options->nameFilters();
391 const int menuIndex = m_popupButton.indexOfSelectedItem;
392 emit m_helper->filterSelected(menuIndex >= 0 && menuIndex < filters.size() ? filters.at(menuIndex) : QString());
395- (QList<QUrl>)selectedFiles
397 if (
auto *openPanel = openpanel_cast(m_panel)) {
399 for (NSURL *url in openPanel.URLs)
400 result << qt_apple_urlFromPossiblySecurityScopedURL(url);
403 QUrl result = qt_apple_urlFromPossiblySecurityScopedURL(m_panel.URL);
404 if (qt_apple_isSandboxed())
407 QFileInfo fileInfo(result.toLocalFile());
409 if (fileInfo.suffix().isEmpty() && ![self fileInfoMatchesCurrentNameFilter:fileInfo]) {
413 fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo];
418 const QString defaultSuffix = m_options->defaultSuffix();
419 if (fileInfo.suffix().isEmpty() && !defaultSuffix.isEmpty()) {
420 fileInfo.setFile(fileInfo.absolutePath(),
421 fileInfo.baseName() +
'.' + defaultSuffix);
424 return { QUrl::fromLocalFile(fileInfo.filePath()) };
428- (
void)updateProperties
430 const QFileDialogOptions::FileMode fileMode = m_options->fileMode();
431 bool chooseFilesOnly = fileMode == QFileDialogOptions::ExistingFile
432 || fileMode == QFileDialogOptions::ExistingFiles;
433 bool chooseDirsOnly = fileMode == QFileDialogOptions::Directory
434 || fileMode == QFileDialogOptions::DirectoryOnly
435 || m_options->testOption(QFileDialogOptions::ShowDirsOnly);
437 m_panel.title = m_options->windowTitle().toNSString();
438 m_panel.canCreateDirectories = !(m_options->testOption(QFileDialogOptions::ReadOnly));
440 if (m_options->isLabelExplicitlySet(QFileDialogOptions::Accept))
441 m_panel.prompt = strippedText(m_options->labelText(QFileDialogOptions::Accept));
442 if (m_options->isLabelExplicitlySet(QFileDialogOptions::FileName))
443 m_panel.nameFieldLabel = strippedText(m_options->labelText(QFileDialogOptions::FileName));
445 if (
auto *openPanel = openpanel_cast(m_panel)) {
446 openPanel.canChooseFiles = !chooseDirsOnly;
447 openPanel.canChooseDirectories = !chooseFilesOnly;
448 openPanel.allowsMultipleSelection = (fileMode == QFileDialogOptions::ExistingFiles);
449 openPanel.resolvesAliases = !(m_options->testOption(QFileDialogOptions::DontResolveSymlinks));
452 m_popupButton.hidden = chooseDirsOnly;
454 m_panel.allowedContentTypes = [self computeAllowedContentTypes];
464 const bool nameFieldHasExtension = m_panel.nameFieldStringValue.pathExtension.length > 0;
465 if (![m_panel.allowedContentTypes count] && !nameFieldHasExtension && !openpanel_cast(m_panel)) {
466 if (!UTTypeDirectory.preferredFilenameExtension) {
467 m_panel.allowedContentTypes = @[ UTTypeDirectory ];
468 m_panel.allowedContentTypes = @[];
470 qWarning() <<
"UTTypeDirectory unexpectedly reported an extension";
474 m_panel.showsHiddenFiles = m_options->filter().testFlag(QDir::Hidden);
477 [m_panel validateVisibleColumns];
480- (
void)panelSelectionDidChange:(id)sender
492 if (!openpanel_cast(m_panel))
495 if (m_panel.visible) {
496 const QString selection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C);
497 if (selection != m_currentSelection) {
498 m_currentSelection = selection;
499 emit m_helper->currentChanged(QUrl::fromLocalFile(selection));
504- (
void)panel:(id)sender directoryDidChange:(NSString *)path
511 m_helper->panelDirectoryDidChange(path);
515
516
517
518
519
520
521
522
523- (NSArray<UTType*>*)computeAllowedContentTypes
525 if (m_options->acceptMode() != QFileDialogOptions::AcceptSave)
528 auto *types = [[NSMutableArray<UTType*>
new] autorelease];
529 for (
const QString &filter : std::as_const(m_selectedNameFilter)) {
530 if (!filter.startsWith(
"*."_L1))
533 if (filter.contains(u'?'))
536 if (filter.count(u'*') != 1)
539 auto extensions = filter.split(
'.', Qt::SkipEmptyParts);
540 if (extensions.count() > 2)
543 auto *utType = [UTType typeWithFilenameExtension:extensions.last().toNSString()];
544 [types addObject:utType];
550- (
QString)removeExtensions:(
const QString &)filter
552 QRegularExpression regExp(QString::fromLatin1(QPlatformFileDialogHelper::filterRegExp));
553 QRegularExpressionMatch match = regExp.match(filter);
554 if (match.hasMatch())
555 return match.captured(1).trimmed();
559- (
void)createTextField
561 NSRect textRect = { { 0.0, 3.0 }, { 100.0, 25.0 } };
562 m_textField = [[NSTextField alloc] initWithFrame:textRect];
563 m_textField.cell.font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSControlSizeRegular]];
564 m_textField.alignment = NSTextAlignmentRight;
565 m_textField.editable =
false;
566 m_textField.selectable =
false;
567 m_textField.bordered =
false;
568 m_textField.drawsBackground =
false;
569 if (m_options->isLabelExplicitlySet(QFileDialogOptions::FileType))
570 m_textField.stringValue = strippedText(m_options->labelText(QFileDialogOptions::FileType));
573- (
void)createPopUpButton:(
const QString &)selectedFilter hideDetails:(BOOL)hideDetails
575 NSRect popUpRect = { { 100.0, 5.0 }, { 250.0, 25.0 } };
576 m_popupButton = [[NSPopUpButton alloc] initWithFrame:popUpRect pullsDown:NO];
577 m_popupButton.target = self;
578 m_popupButton.action = @selector(filterChanged:);
580 if (!m_nameFilterDropDownList.isEmpty()) {
581 int filterToUse = -1;
582 for (
int i = 0; i < m_nameFilterDropDownList.size(); ++i) {
583 const QString currentFilter = m_nameFilterDropDownList.at(i);
584 if (selectedFilter == currentFilter ||
585 (filterToUse == -1 && currentFilter.startsWith(selectedFilter)))
587 QString filter = hideDetails ? [self removeExtensions:currentFilter] : currentFilter;
588 [m_popupButton.menu addItemWithTitle:filter.toNSString() action:nil keyEquivalent:@
""];
590 if (filterToUse != -1)
591 [m_popupButton selectItemAtIndex:filterToUse];
595- (
QStringList) findStrippedFilterWithVisualFilterName:(QString)name
597 for (
const QString ¤tFilter : std::as_const(m_nameFilterDropDownList)) {
598 if (currentFilter.startsWith(name))
599 return QPlatformFileDialogHelper::cleanFilterList(currentFilter);
601 return QStringList();
604- (
void)createAccessory
606 NSRect accessoryRect = { { 0.0, 0.0 }, { 450.0, 33.0 } };
607 m_accessoryView = [[NSView alloc] initWithFrame:accessoryRect];
608 [m_accessoryView addSubview:m_textField];
609 [m_accessoryView addSubview:m_popupButton];
616QCocoaFileDialogHelper::QCocoaFileDialogHelper()
625 QMacAutoReleasePool pool;
626 [m_delegate release];
632 if (result == NSModalResponseOK)
640 m_directory = directory;
643 m_delegate->m_panel.directoryURL = [NSURL fileURLWithPath:directory.toLocalFile().toNSString()];
653 if (!path || [path isEqual:NSNull.null] || !path.length)
656 const auto oldDirectory = m_directory;
657 m_directory = QUrl::fromLocalFile(
658 QString::fromNSString(path).normalized(QString::NormalizationForm_C));
660 if (m_directory != oldDirectory) {
662 emit directoryEntered(m_directory);
668 QString filePath = filename.toLocalFile();
669 if (QDir::isRelativePath(filePath))
670 filePath = QFileInfo(directory().toLocalFile(), filePath).filePath();
674 setDirectory(QFileInfo(filePath).absolutePath());
680 return [m_delegate selectedFiles];
681 return QList<QUrl>();
689 [m_delegate updateProperties];
696 const int index = options()->nameFilters().indexOf(filter);
699 options()->setInitiallySelectedNameFilter(filter);
702 [m_delegate->m_popupButton selectItemAtIndex:index];
703 [m_delegate filterChanged:nil];
710 return options()->initiallySelectedNameFilter();
711 int index = [m_delegate->m_popupButton indexOfSelectedItem];
712 if (index >= options()->nameFilters().count())
714 return index != -1 ? options()->nameFilters().at(index) : QString();
722 [m_delegate closePanel];
730 if (windowFlags & Qt::WindowStaysOnTopHint) {
738 if (qt_apple_isSandboxed()) {
739 static bool canRead = qt_mac_processHasEntitlement(
740 u"com.apple.security.files.user-selected.read-only"_s);
741 static bool canReadWrite = qt_mac_processHasEntitlement(
742 u"com.apple.security.files.user-selected.read-write"_s);
744 if (options()->acceptMode() == QFileDialogOptions::AcceptSave
746 qWarning() <<
"Sandboxed application is missing user-selected files"
747 <<
"read-write entitlement. Falling back to non-native dialog";
751 if (!canReadWrite && !canRead) {
752 qWarning() <<
"Sandboxed application is missing user-selected files"
753 <<
"entitlement. Falling back to non-native dialog";
758 createNSOpenSavePanelDelegate();
760 return [m_delegate showPanel:windowModality withParent:parent];
765 QMacAutoReleasePool pool;
768 const QList<QUrl> selectedFiles = opts->initiallySelectedFiles();
769 const QUrl directory = m_directory.isEmpty() ? opts->initialDirectory() : m_directory;
770 const bool selectDir = selectedFiles.isEmpty();
771 QString selection(selectDir ? directory.toLocalFile() : selectedFiles.front().toLocalFile());
772 QNSOpenSavePanelDelegate *delegate = [[QNSOpenSavePanelDelegate alloc]
778 [
static_cast<QNSOpenSavePanelDelegate *>(m_delegate) release];
779 m_delegate = delegate;
784 Q_ASSERT(m_delegate);
786 if (m_delegate->m_panel.visible) {
788 QEventLoop eventLoop;
789 m_eventLoop = &eventLoop;
790 eventLoop.exec(QEventLoop::DialogExec);
791 m_eventLoop =
nullptr;
794 [m_delegate runApplicationModalPanel];
QList< QUrl > selectedFiles() const override
bool defaultNameFilterDisables() const override
void setFilter() override
QString selectedNameFilter() const override
void panelDirectoryDidChange(NSString *path)
QUrl directory() const override
void setDirectory(const QUrl &directory) override
virtual ~QCocoaFileDialogHelper()
bool show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) override
void selectNameFilter(const QString &filter) override
void selectFile(const QUrl &filename) override
void panelClosed(NSInteger result)
\macro QT_RESTRICTED_CAST_FROM_ASCII
QSharedPointer< QFileDialogOptions > SharedPointerFileDialogOptions
static NSString * strippedText(QString s)
static NSOpenPanel * openpanel_cast(NSSavePanel *panel)
QList< QString > QStringList
Constructs a string list that contains the given string, str.