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