Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qcocoafiledialoghelper.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include <QtCore/qglobal.h>
6
7#include <AppKit/AppKit.h>
8
10#include "qcocoahelpers.h"
12
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>
23
24#include <QtGui/qguiapplication.h>
25#include <QtGui/private/qguiapplication_p.h>
26
27#include <qpa/qplatformtheme.h>
28#include <qpa/qplatformnativeinterface.h>
29
30#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
31
32QT_USE_NAMESPACE
33
34using namespace Qt::StringLiterals;
35
36static NSString *strippedText(QString s)
37{
38 s.remove("..."_L1);
39 return QPlatformTheme::removeMnemonics(s).trimmed().toNSString();
40}
41
42// NSOpenPanel extends NSSavePanel with some extra APIs
43static NSOpenPanel *openpanel_cast(NSSavePanel *panel)
44{
45 if ([panel isKindOfClass:NSOpenPanel.class])
46 return static_cast<NSOpenPanel*>(panel);
47 else
48 return nil;
49}
50
52
53@implementation QNSOpenSavePanelDelegate {
54 @public
55 NSSavePanel *m_panel;
56 NSView *m_accessoryView;
57 NSPopUpButton *m_popupButton;
58 NSTextField *m_textField;
59 QPointer<QCocoaFileDialogHelper> m_helper;
60
61 SharedPointerFileDialogOptions m_options;
62 QString m_currentSelection;
63 QStringList m_nameFilterDropDownList;
64 QStringList m_selectedNameFilter;
65}
66
67- (instancetype)initWithAcceptMode:(const QString &)selectFile
68 options:(SharedPointerFileDialogOptions)options
69 helper:(QCocoaFileDialogHelper *)helper
70{
71 if ((self = [super init])) {
72 m_options = options;
73
74 if (m_options->acceptMode() == QFileDialogOptions::AcceptOpen)
75 m_panel = [[NSOpenPanel openPanel] retain];
76 else
77 m_panel = [[NSSavePanel savePanel] retain];
78
79 m_panel.canSelectHiddenExtension = YES;
80 m_panel.level = NSModalPanelWindowLevel;
81
82 m_helper = helper;
83
84 m_nameFilterDropDownList = m_options->nameFilters();
85 QString selectedVisualNameFilter = m_options->initiallySelectedNameFilter();
86 m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selectedVisualNameFilter];
87
88 m_panel.extensionHidden = [&]{
89 for (const auto &nameFilter : m_nameFilterDropDownList) {
90 const auto extensions = QPlatformFileDialogHelper::cleanFilterList(nameFilter);
91 for (const auto &extension : extensions) {
92 // Explicitly show extensions if we detect a filter
93 // of "all files", as clicking a single file with
94 // extensions hidden will then populate the name
95 // field with only the file name, without any
96 // extension.
97 if (extension == "*"_L1 || extension == "*.*"_L1)
98 return false;
99
100 // Explicitly show extensions if we detect a filter
101 // that has a multi-part extension. This prevents
102 // confusing situations where the user clicks e.g.
103 // 'foo.tar.gz' and 'foo.tar' is populated in the
104 // file name box, but when then clicking save macOS
105 // will warn that the file needs to end in .gz,
106 // due to thinking the user tried to save the file
107 // as a 'tar' file instead. Unfortunately this
108 // property can only be set before the panel is
109 // shown, so we can't toggle it on and off based
110 // on the active filter.
111 if (extension.count('.') > 1)
112 return false;
113 }
114 }
115 return true;
116 }();
117
118 const QFileInfo sel(selectFile);
119 if (sel.isDir() && !sel.isBundle()){
120 m_panel.directoryURL = [NSURL fileURLWithPath:sel.absoluteFilePath().toNSString()];
121 m_currentSelection.clear();
122 } else {
123 m_panel.directoryURL = [NSURL fileURLWithPath:sel.absolutePath().toNSString()];
124 m_currentSelection = sel.absoluteFilePath();
125 }
126
127 [self createPopUpButton:selectedVisualNameFilter hideDetails:options->testOption(QFileDialogOptions::HideNameFilterDetails)];
128 [self createTextField];
129 [self createAccessory];
130
131 m_panel.accessoryView = m_nameFilterDropDownList.size() > 1 ? m_accessoryView : nil;
132 // -setAccessoryView: can result in -panel:directoryDidChange:
133 // resetting our current directory. Set the delegate
134 // here to make sure it gets the correct value.
135 m_panel.delegate = self;
136
137 if (auto *openPanel = openpanel_cast(m_panel))
138 openPanel.accessoryViewDisclosed = YES;
139
140 [self updateProperties];
141 }
142 return self;
143}
144
145- (void)dealloc
146{
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;
153 [m_panel release];
154 [super dealloc];
155}
156
157- (bool)showPanel:(Qt::WindowModality) windowModality withParent:(QWindow *)parent
158{
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];
164
165 if (!openpanel_cast(m_panel))
166 m_panel.nameFieldStringValue = selectable ? info.fileName().toNSString() : @"";
167
168 [self updateProperties];
169
170 auto completionHandler = ^(NSInteger result) {
171 if (m_helper)
172 m_helper->panelClosed(result);
173 };
174
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) {
179 return true; // Defer until exec()
180 } else {
181 [m_panel beginWithCompletionHandler:completionHandler];
182 }
183
184 return true;
185}
186
187-(void)runApplicationModalPanel
188{
189 // Note: If NSApp is not running (which is the case if e.g a top-most
190 // QEventLoop has been interrupted, and the second-most event loop has not
191 // yet been reactivated (regardless if [NSApp run] is still on the stack)),
192 // showing a native modal dialog will fail.
193 if (!m_helper)
194 return;
195
196 QMacAutoReleasePool pool;
197
198 // Call processEvents in case the event dispatcher has been interrupted, and needs to do
199 // cleanup of modal sessions. Do this before showing the native dialog, otherwise it will
200 // close down during the cleanup.
201 qApp->processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers);
202
203 // Make sure we don't interrupt the runModal call below.
204 QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag();
205
206 auto result = [m_panel runModal];
207 m_helper->panelClosed(result);
208
209 // Wake up the event dispatcher so it can check whether the
210 // current event loop should continue spinning or not.
211 QCoreApplication::eventDispatcher()->wakeUp();
212}
213
214- (void)closePanel
215{
216 m_currentSelection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C);
217
218 if (m_panel.sheet)
219 [NSApp endSheet:m_panel];
220 else if (NSApp.modalWindow == m_panel)
221 [NSApp stopModal];
222 else
223 [m_panel close];
224}
225
226- (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url
227{
228 Q_UNUSED(sender);
229
230 NSString *filename = url.path;
231 if (!filename.length)
232 return NO;
233
234 const QFileInfo fileInfo(QString::fromNSString(filename));
235
236 // Always accept directories regardless of their names.
237 // This also includes symlinks and aliases to directories.
238 if (fileInfo.isDir()) {
239 // Unless it's a bundle, and we should treat bundles as files.
240 // FIXME: We'd like to use QFileInfo::isBundle() here, but the
241 // detection in QFileInfo goes deeper than NSWorkspace does
242 // (likely a bug), and as a result causes TCC permission
243 // dialogs to pop up when used.
244 bool treatBundlesAsFiles = !m_panel.treatsFilePackagesAsDirectories;
245 if (!(treatBundlesAsFiles && [NSWorkspace.sharedWorkspace isFilePackageAtPath:filename]))
246 return YES;
247 }
248
249 if (![self fileInfoMatchesCurrentNameFilter:fileInfo])
250 return NO;
251
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()))
256 return NO;
257
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()))
264 return NO;
265 }
266
267 // We control the visibility of hidden files via the showsHiddenFiles
268 // property on the panel, based on QDir::Hidden being set. But the user
269 // can also toggle this via the Command+Shift+. keyboard shortcut,
270 // in which case they have explicitly requested to show hidden files,
271 // and we should enable them even if QDir::Hidden was not set. In
272 // effect, we don't need to filter on QDir::Hidden here.
273
274 return YES;
275}
276
277- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError * _Nullable *)outError
278{
279 Q_ASSERT(sender == m_panel);
280
281 if (![m_panel.allowedContentTypes count] && !m_selectedNameFilter.isEmpty()) {
282 // The save panel hasn't done filtering on our behalf,
283 // either because we couldn't represent the filter via
284 // allowedContentTypes, or we opted out due to a multi part
285 // extension, so do the filtering/validation ourselves.
286 QFileInfo fileInfo(QString::fromNSString(url.path).normalized(QString::NormalizationForm_C));
287
288 if ([self fileInfoMatchesCurrentNameFilter:fileInfo])
289 return YES;
290
291 if (fileInfo.suffix().isEmpty()) {
292 // The filter requires a file name with an extension.
293 // We're going to add a default file name in selectedFiles,
294 // to match the native behavior. Check now that we can
295 // overwrite the file, if is already exists.
296 fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo];
297
298 if (!fileInfo.exists() || m_options->testOption(QFileDialogOptions::DontConfirmOverwrite))
299 return YES;
300
301 QMacAutoReleasePool pool;
302 auto *alert = [[NSAlert new] autorelease];
303 alert.alertStyle = NSAlertStyleCritical;
304
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()];
312
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")];
317
318 [alert beginSheetModalForWindow:m_panel
319 completionHandler:^(NSModalResponse returnCode) {
320 [NSApp stopModalWithCode:returnCode];
321 }];
322 return [NSApp runModalForWindow:alert.window] == replaceButton.tag;
323 } else {
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()]
331 }];
332 return NO;
333 }
334 }
335
336 return YES;
337}
338
339- (QFileInfo)applyDefaultSuffixFromCurrentNameFilter:(const QFileInfo &)fileInfo
340{
341 QFileInfo filterInfo(m_selectedNameFilter.first());
342 return QFileInfo(fileInfo.absolutePath(),
343 fileInfo.baseName() + '.' + filterInfo.completeSuffix());
344}
345
346- (bool)fileInfoMatchesCurrentNameFilter:(const QFileInfo &)fileInfo
347{
348 // No filter means accept everything
349 if (m_selectedNameFilter.isEmpty())
350 return true;
351
352 // Check if the current file name filter accepts the file
353 for (const auto &filter : m_selectedNameFilter) {
354 if (QDir::match(filter, fileInfo.fileName()))
355 return true;
356 }
357
358 return false;
359}
360
361- (void)setNameFilters:(const QStringList &)filters hideDetails:(BOOL)hideDetails
362{
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:@""];
369 }
370 [m_popupButton selectItemAtIndex:0];
371 m_panel.accessoryView = m_accessoryView;
372 } else {
373 m_panel.accessoryView = nil;
374 }
375
376 [self filterChanged:self];
377}
378
379- (void)filterChanged:(id)sender
380{
381 // This m_delegate function is called when the _name_ filter changes.
382 Q_UNUSED(sender);
383 if (!m_helper)
384 return;
385 const QString selection = m_nameFilterDropDownList.value([m_popupButton indexOfSelectedItem]);
386 m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection];
387
388 // Don't touch the panel if it's not shown, as its remote view service
389 // may already be invalidated (see QCocoaFileDialogHelper::setDirectory).
390 if (m_panel.visible) {
391 [m_panel validateVisibleColumns];
392 [self updateProperties];
393 }
394
395 const QStringList filters = m_options->nameFilters();
396 const int menuIndex = m_popupButton.indexOfSelectedItem;
397 emit m_helper->filterSelected(menuIndex >= 0 && menuIndex < filters.size() ? filters.at(menuIndex) : QString());
398}
399
400- (QList<QUrl>)selectedFiles
401{
402 if (auto *openPanel = openpanel_cast(m_panel)) {
403 QList<QUrl> result;
404 for (NSURL *url in openPanel.URLs)
405 result << qt_apple_urlFromPossiblySecurityScopedURL(url);
406 return result;
407 } else {
408 QUrl result = qt_apple_urlFromPossiblySecurityScopedURL(m_panel.URL);
409 if (qt_apple_isSandboxed())
410 return { result }; // Can't tweak suffix
411
412 QFileInfo fileInfo(result.toLocalFile());
413
414 if (fileInfo.suffix().isEmpty() && ![self fileInfoMatchesCurrentNameFilter:fileInfo]) {
415 // We end up in this situation if we accept a file name without extension
416 // in panel:validateURL:error. If so, we match the behavior of the native
417 // save dialog and add the first of the accepted extension from the filter.
418 fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo];
419 }
420
421 // If neither the user or the NSSavePanel have provided a suffix, use
422 // the default suffix (if it exists).
423 const QString defaultSuffix = m_options->defaultSuffix();
424 if (fileInfo.suffix().isEmpty() && !defaultSuffix.isEmpty()) {
425 fileInfo.setFile(fileInfo.absolutePath(),
426 fileInfo.baseName() + '.' + defaultSuffix);
427 }
428
429 return { QUrl::fromLocalFile(fileInfo.filePath()) };
430 }
431}
432
433- (void)updateProperties
434{
435 const QFileDialogOptions::FileMode fileMode = m_options->fileMode();
436 bool chooseFilesOnly = fileMode == QFileDialogOptions::ExistingFile
437 || fileMode == QFileDialogOptions::ExistingFiles;
438 bool chooseDirsOnly = fileMode == QFileDialogOptions::Directory
439 || fileMode == QFileDialogOptions::DirectoryOnly
440 || m_options->testOption(QFileDialogOptions::ShowDirsOnly);
441
442 m_panel.title = m_options->windowTitle().toNSString();
443 m_panel.canCreateDirectories = !(m_options->testOption(QFileDialogOptions::ReadOnly));
444
445 if (m_options->isLabelExplicitlySet(QFileDialogOptions::Accept))
446 m_panel.prompt = strippedText(m_options->labelText(QFileDialogOptions::Accept));
447 if (m_options->isLabelExplicitlySet(QFileDialogOptions::FileName))
448 m_panel.nameFieldLabel = strippedText(m_options->labelText(QFileDialogOptions::FileName));
449
450 if (auto *openPanel = openpanel_cast(m_panel)) {
451 openPanel.canChooseFiles = !chooseDirsOnly;
452 openPanel.canChooseDirectories = !chooseFilesOnly;
453 openPanel.allowsMultipleSelection = (fileMode == QFileDialogOptions::ExistingFiles);
454 openPanel.resolvesAliases = !(m_options->testOption(QFileDialogOptions::DontResolveSymlinks));
455 }
456
457 m_popupButton.hidden = chooseDirsOnly; // TODO hide the whole sunken pane instead?
458
459 m_panel.allowedContentTypes = [self computeAllowedContentTypes];
460
461 // Setting allowedContentTypes to @[] is not enough to reset any
462 // automatically added extension based on a previous filter.
463 // This is problematic because extensions can in some cases
464 // be hidden from the user, resulting in confusion when the
465 // resulting file name doesn't match the current empty filter.
466 // We work around this by temporarily resetting the allowed
467 // content type to one without an extension, which forces
468 // the save panel to update and remove the extension.
469 const bool nameFieldHasExtension = m_panel.nameFieldStringValue.pathExtension.length > 0;
470 if (![m_panel.allowedContentTypes count] && !nameFieldHasExtension && !openpanel_cast(m_panel)) {
471 if (!UTTypeDirectory.preferredFilenameExtension) {
472 m_panel.allowedContentTypes = @[ UTTypeDirectory ];
473 m_panel.allowedContentTypes = @[];
474 } else {
475 qWarning() << "UTTypeDirectory unexpectedly reported an extension";
476 }
477 }
478
479 m_panel.showsHiddenFiles = m_options->filter().testFlag(QDir::Hidden);
480
481 if (m_panel.visible)
482 [m_panel validateVisibleColumns];
483}
484
485- (void)panelSelectionDidChange:(id)sender
486{
487 Q_UNUSED(sender);
488
489 if (!m_helper)
490 return;
491
492 // Save panels only allow you to select directories, which
493 // means currentChanged will only be emitted when selecting
494 // a directory, and if so, with the latest chosen file name,
495 // which is confusing and inconsistent. We choose to bail
496 // out entirely for save panels, to give consistent behavior.
497 if (!openpanel_cast(m_panel))
498 return;
499
500 if (m_panel.visible) {
501 const QString selection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C);
502 if (selection != m_currentSelection) {
503 m_currentSelection = selection;
504 emit m_helper->currentChanged(QUrl::fromLocalFile(selection));
505 }
506 }
507}
508
509- (void)panel:(id)sender directoryDidChange:(NSString *)path
510{
511 Q_UNUSED(sender);
512
513 if (!m_helper)
514 return;
515
516 m_helper->panelDirectoryDidChange(path);
517}
518
519/*
520 Computes a list of UTTypes ("public.plain-text" e.g.)
521 for the current name filter.
522
523 If a filter do not conform to the format *.xyz or * or *.*,
524 or contains an extensions with more than one part (e.g. "tar.gz")
525 we treat that as allowing all file types, and do our own
526 validation in panel:validateURL:error.
527*/
528- (NSArray<UTType*>*)computeAllowedContentTypes
529{
530 if (m_options->acceptMode() != QFileDialogOptions::AcceptSave)
531 return @[]; // panel:shouldEnableURL: does the file filtering for NSOpenPanel
532
533 auto *types = [[NSMutableArray<UTType*> new] autorelease];
534 for (const QString &filter : std::as_const(m_selectedNameFilter)) {
535 if (!filter.startsWith("*."_L1))
536 continue;
537
538 if (filter.contains(u'?'))
539 continue;
540
541 if (filter.count(u'*') != 1)
542 continue;
543
544 auto extensions = filter.split('.', Qt::SkipEmptyParts);
545 if (extensions.count() > 2)
546 return @[];
547
548 auto *utType = [UTType typeWithFilenameExtension:extensions.last().toNSString()];
549 [types addObject:utType];
550 }
551
552 return types;
553}
554
555- (QString)removeExtensions:(const QString &)filter
556{
557 QRegularExpression regExp(QString::fromLatin1(QPlatformFileDialogHelper::filterRegExp));
558 QRegularExpressionMatch match = regExp.match(filter);
559 if (match.hasMatch())
560 return match.captured(1).trimmed();
561 return filter;
562}
563
564- (void)createTextField
565{
566 NSRect textRect = { { 0.0, 3.0 }, { 100.0, 25.0 } };
567 m_textField = [[NSTextField alloc] initWithFrame:textRect];
568 m_textField.cell.font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSControlSizeRegular]];
569 m_textField.alignment = NSTextAlignmentRight;
570 m_textField.editable = false;
571 m_textField.selectable = false;
572 m_textField.bordered = false;
573 m_textField.drawsBackground = false;
574 if (m_options->isLabelExplicitlySet(QFileDialogOptions::FileType))
575 m_textField.stringValue = strippedText(m_options->labelText(QFileDialogOptions::FileType));
576}
577
578- (void)createPopUpButton:(const QString &)selectedFilter hideDetails:(BOOL)hideDetails
579{
580 NSRect popUpRect = { { 100.0, 5.0 }, { 250.0, 25.0 } };
581 m_popupButton = [[NSPopUpButton alloc] initWithFrame:popUpRect pullsDown:NO];
582 m_popupButton.target = self;
583 m_popupButton.action = @selector(filterChanged:);
584
585 if (!m_nameFilterDropDownList.isEmpty()) {
586 int filterToUse = -1;
587 for (int i = 0; i < m_nameFilterDropDownList.size(); ++i) {
588 const QString currentFilter = m_nameFilterDropDownList.at(i);
589 if (selectedFilter == currentFilter ||
590 (filterToUse == -1 && currentFilter.startsWith(selectedFilter)))
591 filterToUse = i;
592 QString filter = hideDetails ? [self removeExtensions:currentFilter] : currentFilter;
593 [m_popupButton.menu addItemWithTitle:filter.toNSString() action:nil keyEquivalent:@""];
594 }
595 if (filterToUse != -1)
596 [m_popupButton selectItemAtIndex:filterToUse];
597 }
598}
599
600- (QStringList) findStrippedFilterWithVisualFilterName:(QString)name
601{
602 for (const QString &currentFilter : std::as_const(m_nameFilterDropDownList)) {
603 if (currentFilter.startsWith(name))
604 return QPlatformFileDialogHelper::cleanFilterList(currentFilter);
605 }
606 return QStringList();
607}
608
609- (void)createAccessory
610{
611 NSRect accessoryRect = { { 0.0, 0.0 }, { 450.0, 33.0 } };
612 m_accessoryView = [[NSView alloc] initWithFrame:accessoryRect];
613 [m_accessoryView addSubview:m_textField];
614 [m_accessoryView addSubview:m_popupButton];
615}
616
617@end
618
619QT_BEGIN_NAMESPACE
620
621QCocoaFileDialogHelper::QCocoaFileDialogHelper()
622{
623}
624
626{
627 if (!m_delegate)
628 return;
629
630 QMacAutoReleasePool pool;
631 [m_delegate release];
632 m_delegate = nil;
633}
634
635void QCocoaFileDialogHelper::panelClosed(NSInteger result)
636{
637 if (result == NSModalResponseOK)
638 emit accept();
639 else
640 emit reject();
641}
642
643void QCocoaFileDialogHelper::setDirectory(const QUrl &directory)
644{
645 m_directory = directory;
646
647 // Only update the panel if it's currently shown. A previously closed
648 // panel is kept alive (for selectedFiles), but its remote view service
649 // may already be invalidated, in which case AppKit throws an
650 // NSInternalInconsistencyException. The next show() creates a new
651 // panel that picks up m_directory anyway.
652 if (m_delegate && m_delegate->m_panel.visible)
653 m_delegate->m_panel.directoryURL = [NSURL fileURLWithPath:directory.toLocalFile().toNSString()];
654}
655
657{
658 return m_directory;
659}
660
662{
663 if (!path || [path isEqual:NSNull.null] || !path.length)
664 return;
665
666 const auto oldDirectory = m_directory;
667 m_directory = QUrl::fromLocalFile(
668 QString::fromNSString(path).normalized(QString::NormalizationForm_C));
669
670 if (m_directory != oldDirectory) {
671 // FIXME: Plumb old directory back to QFileDialog's lastVisitedDir?
672 emit directoryEntered(m_directory);
673 }
674}
675
676void QCocoaFileDialogHelper::selectFile(const QUrl &filename)
677{
678 // Non-local URLs result in an empty path below, which would then
679 // resolve to the parent of the current directory.
680 if (!isSupportedUrl(filename))
681 return;
682
683 QString filePath = filename.toLocalFile();
684 if (QDir::isRelativePath(filePath))
685 filePath = QFileInfo(directory().toLocalFile(), filePath).filePath();
686
687 // There seems to no way to select a file once the dialog is running.
688 // So do the next best thing, set the file's directory:
689 setDirectory(QUrl::fromLocalFile(QFileInfo(filePath).absolutePath()));
690}
691
693{
694 if (m_delegate)
695 return [m_delegate selectedFiles];
696 return QList<QUrl>();
697}
698
700{
701 // Don't touch the panel if it's not shown (see setDirectory). The next
702 // show() creates a new panel that picks up the current options anyway.
703 if (!m_delegate || !m_delegate->m_panel.visible)
704 return;
705
706 [m_delegate updateProperties];
707}
708
709void QCocoaFileDialogHelper::selectNameFilter(const QString &filter)
710{
711 if (!options())
712 return;
713 const int index = options()->nameFilters().indexOf(filter);
714 if (index != -1) {
715 if (!m_delegate) {
716 options()->setInitiallySelectedNameFilter(filter);
717 return;
718 }
719 [m_delegate->m_popupButton selectItemAtIndex:index];
720 [m_delegate filterChanged:nil];
721 }
722}
723
725{
726 if (!m_delegate)
727 return options()->initiallySelectedNameFilter();
728 int index = [m_delegate->m_popupButton indexOfSelectedItem];
729 if (index >= options()->nameFilters().count())
730 return QString();
731 return index != -1 ? options()->nameFilters().at(index) : QString();
732}
733
735{
736 if (!m_delegate)
737 return;
738
739 [m_delegate closePanel];
740
741 if (m_eventLoop)
742 m_eventLoop->exit();
743}
744
745bool QCocoaFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
746{
747 if (windowFlags & Qt::WindowStaysOnTopHint) {
748 // The native file dialog tries all it can to stay
749 // on the NSModalPanel level. And it might also show
750 // its own "create directory" dialog that we cannot control.
751 // So we need to use the non-native version in this case...
752 return false;
753 }
754
755 if (qt_apple_isSandboxed()) {
756 static bool canRead = qt_mac_processHasEntitlement(
757 u"com.apple.security.files.user-selected.read-only"_s);
758 static bool canReadWrite = qt_mac_processHasEntitlement(
759 u"com.apple.security.files.user-selected.read-write"_s);
760
761 if (options()->acceptMode() == QFileDialogOptions::AcceptSave
762 && !canReadWrite) {
763 qWarning() << "Sandboxed application is missing user-selected files"
764 << "read-write entitlement. Falling back to non-native dialog";
765 return false;
766 }
767
768 if (!canReadWrite && !canRead) {
769 qWarning() << "Sandboxed application is missing user-selected files"
770 << "entitlement. Falling back to non-native dialog";
771 return false;
772 }
773 }
774
775 createNSOpenSavePanelDelegate();
776
777 return [m_delegate showPanel:windowModality withParent:parent];
778}
779
780void QCocoaFileDialogHelper::createNSOpenSavePanelDelegate()
781{
782 QMacAutoReleasePool pool;
783
784 const SharedPointerFileDialogOptions &opts = options();
785 const QList<QUrl> selectedFiles = opts->initiallySelectedFiles();
786 const QUrl directory = m_directory.isEmpty() ? opts->initialDirectory() : m_directory;
787 const bool selectDir = selectedFiles.isEmpty();
788 QString selection(selectDir ? directory.toLocalFile() : selectedFiles.front().toLocalFile());
789 QNSOpenSavePanelDelegate *delegate = [[QNSOpenSavePanelDelegate alloc]
790 initWithAcceptMode:
791 selection
792 options:opts
793 helper:this];
794
795 [static_cast<QNSOpenSavePanelDelegate *>(m_delegate) release];
796 m_delegate = delegate;
797}
798
800{
801 Q_ASSERT(m_delegate);
802
803 if (m_delegate->m_panel.visible) {
804 // WindowModal or NonModal, so already shown above
805 QEventLoop eventLoop;
806 m_eventLoop = &eventLoop;
807 eventLoop.exec(QEventLoop::DialogExec);
808 m_eventLoop = nullptr;
809 } else {
810 // ApplicationModal, so show and block using native APIs
811 [m_delegate runApplicationModalPanel];
812 }
813}
814
816{
817 return true;
818}
819
820QT_END_NAMESPACE
QList< QUrl > selectedFiles() const override
bool defaultNameFilterDisables() const override
QString selectedNameFilter() const override
void panelDirectoryDidChange(NSString *path)
QUrl directory() const override
void setDirectory(const QUrl &directory) override
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
Definition qstring.h:177
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.