1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtGui module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include <qpa/qplatformtheme.h>
41
42#include "qcocoafiledialoghelper.h"
43
44/*****************************************************************************
45  QFileDialog debug facilities
46 *****************************************************************************/
47//#define DEBUG_FILEDIALOG_FILTERS
48
49#include <qguiapplication.h>
50#include <private/qguiapplication_p.h>
51#include "qcocoahelpers.h"
52#include "qcocoaeventdispatcher.h"
53#include <qbuffer.h>
54#include <qdebug.h>
55#include <qstringlist.h>
56#include <qvarlengtharray.h>
57#include <stdlib.h>
58#include <qabstracteventdispatcher.h>
59#include <qsysinfo.h>
60#include <qoperatingsystemversion.h>
61#include <qglobal.h>
62#include <qdir.h>
63#include <qregularexpression.h>
64
65#include <qpa/qplatformnativeinterface.h>
66
67#import <AppKit/NSSavePanel.h>
68#import <CoreFoundation/CFNumber.h>
69
70QT_FORWARD_DECLARE_CLASS(QString)
71QT_FORWARD_DECLARE_CLASS(QStringList)
72QT_FORWARD_DECLARE_CLASS(QFileInfo)
73QT_FORWARD_DECLARE_CLASS(QWindow)
74QT_USE_NAMESPACE
75
76typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions;
77
78@implementation QNSOpenSavePanelDelegate {
79    @public
80    NSOpenPanel *mOpenPanel;
81    NSSavePanel *mSavePanel;
82    NSView *mAccessoryView;
83    NSPopUpButton *mPopUpButton;
84    NSTextField *mTextField;
85    QCocoaFileDialogHelper *mHelper;
86    NSString *mCurrentDir;
87
88    int mReturnCode;
89
90    SharedPointerFileDialogOptions mOptions;
91    QString *mCurrentSelection;
92    QStringList *mNameFilterDropDownList;
93    QStringList *mSelectedNameFilter;
94}
95
96- (instancetype)initWithAcceptMode:(const QString &)selectFile
97                           options:(SharedPointerFileDialogOptions)options
98                            helper:(QCocoaFileDialogHelper *)helper
99{
100    self = [super init];
101    mOptions = options;
102    if (mOptions->acceptMode() == QFileDialogOptions::AcceptOpen){
103        mOpenPanel = [NSOpenPanel openPanel];
104        mSavePanel = mOpenPanel;
105    } else {
106        mSavePanel = [NSSavePanel savePanel];
107        [mSavePanel setCanSelectHiddenExtension:YES];
108        mOpenPanel = nil;
109    }
110
111    if ([mSavePanel respondsToSelector:@selector(setLevel:)])
112        [mSavePanel setLevel:NSModalPanelWindowLevel];
113
114    mReturnCode = -1;
115    mHelper = helper;
116    mNameFilterDropDownList = new QStringList(mOptions->nameFilters());
117    QString selectedVisualNameFilter = mOptions->initiallySelectedNameFilter();
118    mSelectedNameFilter = new QStringList([self findStrippedFilterWithVisualFilterName:selectedVisualNameFilter]);
119
120    QFileInfo sel(selectFile);
121    if (sel.isDir() && !sel.isBundle()){
122        mCurrentDir = [sel.absoluteFilePath().toNSString() retain];
123        mCurrentSelection = new QString;
124    } else {
125        mCurrentDir = [sel.absolutePath().toNSString() retain];
126        mCurrentSelection = new QString(sel.absoluteFilePath());
127    }
128
129    [mSavePanel setTitle:options->windowTitle().toNSString()];
130    [self createPopUpButton:selectedVisualNameFilter hideDetails:options->testOption(QFileDialogOptions::HideNameFilterDetails)];
131    [self createTextField];
132    [self createAccessory];
133    [mSavePanel setAccessoryView:mNameFilterDropDownList->size() > 1 ? mAccessoryView : nil];
134    // -setAccessoryView: can result in -panel:directoryDidChange:
135    // resetting our mCurrentDir, set the delegate
136    // here to make sure it gets the correct value.
137    [mSavePanel setDelegate:self];
138    mOpenPanel.accessoryViewDisclosed = YES;
139
140    if (mOptions->isLabelExplicitlySet(QFileDialogOptions::Accept))
141        [mSavePanel setPrompt:[self strip:options->labelText(QFileDialogOptions::Accept)]];
142    if (mOptions->isLabelExplicitlySet(QFileDialogOptions::FileName))
143        [mSavePanel setNameFieldLabel:[self strip:options->labelText(QFileDialogOptions::FileName)]];
144
145    [self updateProperties];
146    [mSavePanel retain];
147    return self;
148}
149
150- (void)dealloc
151{
152    delete mNameFilterDropDownList;
153    delete mSelectedNameFilter;
154    delete mCurrentSelection;
155
156    if ([mSavePanel respondsToSelector:@selector(orderOut:)])
157        [mSavePanel orderOut:mSavePanel];
158    [mSavePanel setAccessoryView:nil];
159    [mPopUpButton release];
160    [mTextField release];
161    [mAccessoryView release];
162    [mSavePanel setDelegate:nil];
163    [mSavePanel release];
164    [mCurrentDir release];
165    [super dealloc];
166}
167
168static QString strippedText(QString s)
169{
170    s.remove(QLatin1String("..."));
171    return QPlatformTheme::removeMnemonics(s).trimmed();
172}
173
174- (NSString *)strip:(const QString &)label
175{
176    return strippedText(label).toNSString();
177}
178
179- (void)closePanel
180{
181    *mCurrentSelection = QString::fromNSString([[mSavePanel URL] path]).normalized(QString::NormalizationForm_C);
182    if ([mSavePanel respondsToSelector:@selector(close)])
183        [mSavePanel close];
184    if ([mSavePanel isSheet])
185        [NSApp endSheet: mSavePanel];
186}
187
188- (void)showModelessPanel
189{
190    if (mOpenPanel){
191        QFileInfo info(*mCurrentSelection);
192        NSString *filepath = info.filePath().toNSString();
193        NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()];
194        bool selectable = (mOptions->acceptMode() == QFileDialogOptions::AcceptSave)
195            || [self panel:mOpenPanel shouldEnableURL:url];
196
197        [self updateProperties];
198        [mSavePanel setNameFieldStringValue:selectable ? info.fileName().toNSString() : @""];
199
200        [mOpenPanel beginWithCompletionHandler:^(NSInteger result){
201            mReturnCode = result;
202            if (mHelper)
203                mHelper->QNSOpenSavePanelDelegate_panelClosed(result == NSModalResponseOK);
204        }];
205    }
206}
207
208- (BOOL)runApplicationModalPanel
209{
210    QFileInfo info(*mCurrentSelection);
211    NSString *filepath = info.filePath().toNSString();
212    NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()];
213    bool selectable = (mOptions->acceptMode() == QFileDialogOptions::AcceptSave)
214        || [self panel:mSavePanel shouldEnableURL:url];
215
216    [mSavePanel setDirectoryURL: [NSURL fileURLWithPath:mCurrentDir]];
217    [mSavePanel setNameFieldStringValue:selectable ? info.fileName().toNSString() : @""];
218
219    // Call processEvents in case the event dispatcher has been interrupted, and needs to do
220    // cleanup of modal sessions. Do this before showing the native dialog, otherwise it will
221    // close down during the cleanup.
222    qApp->processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers);
223
224    // Make sure we don't interrupt the runModal call below.
225    QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag();
226
227    mReturnCode = [mSavePanel runModal];
228
229    QAbstractEventDispatcher::instance()->interrupt();
230    return (mReturnCode == NSModalResponseOK);
231}
232
233- (QPlatformDialogHelper::DialogCode)dialogResultCode
234{
235    return (mReturnCode == NSModalResponseOK) ? QPlatformDialogHelper::Accepted : QPlatformDialogHelper::Rejected;
236}
237
238- (void)showWindowModalSheet:(QWindow *)parent
239{
240    QFileInfo info(*mCurrentSelection);
241    NSString *filepath = info.filePath().toNSString();
242    NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()];
243    bool selectable = (mOptions->acceptMode() == QFileDialogOptions::AcceptSave)
244        || [self panel:mSavePanel shouldEnableURL:url];
245
246    [self updateProperties];
247    [mSavePanel setDirectoryURL: [NSURL fileURLWithPath:mCurrentDir]];
248
249    [mSavePanel setNameFieldStringValue:selectable ? info.fileName().toNSString() : @""];
250    NSWindow *nsparent = static_cast<NSWindow *>(qGuiApp->platformNativeInterface()->nativeResourceForWindow("nswindow", parent));
251
252    [mSavePanel beginSheetModalForWindow:nsparent completionHandler:^(NSInteger result){
253        mReturnCode = result;
254        if (mHelper)
255            mHelper->QNSOpenSavePanelDelegate_panelClosed(result == NSModalResponseOK);
256    }];
257}
258
259- (BOOL)isHiddenFileAtURL:(NSURL *)url
260{
261    BOOL hidden = NO;
262    if (url) {
263        CFBooleanRef isHiddenProperty;
264        if (CFURLCopyResourcePropertyForKey((__bridge CFURLRef)url, kCFURLIsHiddenKey, &isHiddenProperty, nullptr)) {
265            hidden = CFBooleanGetValue(isHiddenProperty);
266            CFRelease(isHiddenProperty);
267        }
268    }
269    return hidden;
270}
271
272- (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url
273{
274    Q_UNUSED(sender);
275
276    NSString *filename = [url path];
277    if ([filename length] == 0)
278        return NO;
279
280    // Always accept directories regardless of their names (unless it is a bundle):
281    NSFileManager *fm = [NSFileManager defaultManager];
282    NSDictionary *fileAttrs = [fm attributesOfItemAtPath:filename error:nil];
283    if (!fileAttrs)
284        return NO; // Error accessing the file means 'no'.
285    NSString *fileType = [fileAttrs fileType];
286    bool isDir = [fileType isEqualToString:NSFileTypeDirectory];
287    if (isDir) {
288        if ([mSavePanel treatsFilePackagesAsDirectories] == NO) {
289            if ([[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename] == NO)
290                return YES;
291        }
292    }
293
294    QString qtFileName = QFileInfo(QString::fromNSString(filename)).fileName();
295    // No filter means accept everything
296    bool nameMatches = mSelectedNameFilter->isEmpty();
297    // Check if the current file name filter accepts the file:
298    for (int i = 0; !nameMatches && i < mSelectedNameFilter->size(); ++i) {
299        if (QDir::match(mSelectedNameFilter->at(i), qtFileName))
300            nameMatches = true;
301    }
302    if (!nameMatches)
303        return NO;
304
305    QDir::Filters filter = mOptions->filter();
306    if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && isDir)
307        || (!(filter & QDir::Files) && [fileType isEqualToString:NSFileTypeRegular])
308        || ((filter & QDir::NoSymLinks) && [fileType isEqualToString:NSFileTypeSymbolicLink]))
309        return NO;
310
311    bool filterPermissions = ((filter & QDir::PermissionMask)
312                              && (filter & QDir::PermissionMask) != QDir::PermissionMask);
313    if (filterPermissions) {
314        if ((!(filter & QDir::Readable) && [fm isReadableFileAtPath:filename])
315            || (!(filter & QDir::Writable) && [fm isWritableFileAtPath:filename])
316            || (!(filter & QDir::Executable) && [fm isExecutableFileAtPath:filename]))
317            return NO;
318    }
319    if (!(filter & QDir::Hidden)
320        && (qtFileName.startsWith(QLatin1Char('.')) || [self isHiddenFileAtURL:url]))
321            return NO;
322
323    return YES;
324}
325
326- (NSString *)panel:(id)sender userEnteredFilename:(NSString *)filename confirmed:(BOOL)okFlag
327{
328    Q_UNUSED(sender);
329    if (!okFlag)
330        return filename;
331    if (!mOptions->testOption(QFileDialogOptions::DontConfirmOverwrite))
332        return filename;
333
334    // User has clicked save, and no overwrite confirmation should occur.
335    // To get the latter, we need to change the name we return (hence the prefix):
336    return [@"___qt_very_unlikely_prefix_" stringByAppendingString:filename];
337}
338
339- (void)setNameFilters:(const QStringList &)filters hideDetails:(BOOL)hideDetails
340{
341    [mPopUpButton removeAllItems];
342    *mNameFilterDropDownList = filters;
343    if (filters.size() > 0){
344        for (int i=0; i<filters.size(); ++i) {
345            QString filter = hideDetails ? [self removeExtensions:filters.at(i)] : filters.at(i);
346            [mPopUpButton addItemWithTitle:filter.toNSString()];
347        }
348        [mPopUpButton selectItemAtIndex:0];
349        [mSavePanel setAccessoryView:mAccessoryView];
350    } else
351        [mSavePanel setAccessoryView:nil];
352
353    [self filterChanged:self];
354}
355
356- (void)filterChanged:(id)sender
357{
358    // This mDelegate function is called when the _name_ filter changes.
359    Q_UNUSED(sender);
360    QString selection = mNameFilterDropDownList->value([mPopUpButton indexOfSelectedItem]);
361    *mSelectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection];
362    if ([mSavePanel respondsToSelector:@selector(validateVisibleColumns:)])
363        [mSavePanel validateVisibleColumns];
364    [self updateProperties];
365    if (mHelper)
366        mHelper->QNSOpenSavePanelDelegate_filterSelected([mPopUpButton indexOfSelectedItem]);
367}
368
369- (QString)currentNameFilter
370{
371    return mNameFilterDropDownList->value([mPopUpButton indexOfSelectedItem]);
372}
373
374- (QList<QUrl>)selectedFiles
375{
376    if (mOpenPanel) {
377        QList<QUrl> result;
378        NSArray<NSURL *> *array = [mOpenPanel URLs];
379        for (NSURL *url in array) {
380            QString path = QString::fromNSString(url.path).normalized(QString::NormalizationForm_C);
381            result << QUrl::fromLocalFile(path);
382        }
383        return result;
384    } else {
385        QList<QUrl> result;
386        QString filename = QString::fromNSString([[mSavePanel URL] path]).normalized(QString::NormalizationForm_C);
387        const QString defaultSuffix = mOptions->defaultSuffix();
388        const QFileInfo fileInfo(filename);
389        // If neither the user or the NSSavePanel have provided a suffix, use
390        // the default suffix (if it exists).
391        if (fileInfo.suffix().isEmpty() && !defaultSuffix.isEmpty()) {
392                filename.append('.').append(defaultSuffix);
393        }
394        result << QUrl::fromLocalFile(filename.remove(QLatin1String("___qt_very_unlikely_prefix_")));
395        return result;
396    }
397}
398
399- (void)updateProperties
400{
401    // Call this functions if mFileMode, mFileOptions,
402    // mNameFilterDropDownList or mQDirFilter changes.
403    // The savepanel does not contain the necessary functions for this.
404    const QFileDialogOptions::FileMode fileMode = mOptions->fileMode();
405    bool chooseFilesOnly = fileMode == QFileDialogOptions::ExistingFile
406        || fileMode == QFileDialogOptions::ExistingFiles;
407    bool chooseDirsOnly = fileMode == QFileDialogOptions::Directory
408        || fileMode == QFileDialogOptions::DirectoryOnly
409        || mOptions->testOption(QFileDialogOptions::ShowDirsOnly);
410
411    [mOpenPanel setCanChooseFiles:!chooseDirsOnly];
412    [mOpenPanel setCanChooseDirectories:!chooseFilesOnly];
413    [mSavePanel setCanCreateDirectories:!(mOptions->testOption(QFileDialogOptions::ReadOnly))];
414    [mOpenPanel setAllowsMultipleSelection:(fileMode == QFileDialogOptions::ExistingFiles)];
415    [mOpenPanel setResolvesAliases:!(mOptions->testOption(QFileDialogOptions::DontResolveSymlinks))];
416    [mOpenPanel setTitle:mOptions->windowTitle().toNSString()];
417    [mSavePanel setTitle:mOptions->windowTitle().toNSString()];
418    [mPopUpButton setHidden:chooseDirsOnly];    // TODO hide the whole sunken pane instead?
419
420    if (mOptions->acceptMode() == QFileDialogOptions::AcceptSave) {
421        [self recomputeAcceptableExtensionsForSave];
422    } else {
423        [mOpenPanel setAllowedFileTypes:nil]; // delegate panel:shouldEnableURL: does the file filtering for NSOpenPanel
424    }
425
426    if ([mSavePanel respondsToSelector:@selector(isVisible)] && [mSavePanel isVisible]) {
427        if ([mSavePanel respondsToSelector:@selector(validateVisibleColumns)])
428            [mSavePanel validateVisibleColumns];
429    }
430}
431
432- (void)panelSelectionDidChange:(id)sender
433{
434    Q_UNUSED(sender);
435    if (mHelper && [mSavePanel isVisible]) {
436        QString selection = QString::fromNSString([[mSavePanel URL] path]);
437        if (selection != mCurrentSelection) {
438            *mCurrentSelection = selection;
439            mHelper->QNSOpenSavePanelDelegate_selectionChanged(selection);
440        }
441    }
442}
443
444- (void)panel:(id)sender directoryDidChange:(NSString *)path
445{
446    Q_UNUSED(sender);
447    if (!mHelper)
448        return;
449    if (!(path && path.length) || [path isEqualToString:mCurrentDir])
450        return;
451
452    [mCurrentDir release];
453    mCurrentDir = [path retain];
454    mHelper->QNSOpenSavePanelDelegate_directoryEntered(QString::fromNSString(mCurrentDir));
455}
456
457/*
458    Computes a list of extensions (e.g. "png", "jpg", "gif")
459    for the current name filter, and updates the save panel.
460
461    If a filter do not conform to the format *.xyz or * or *.*,
462    all files types are allowed.
463
464    Extensions with more than one part (e.g. "tar.gz") are
465    reduced to their final part, as NSSavePanel does not deal
466    well with multi-part extensions.
467*/
468- (void)recomputeAcceptableExtensionsForSave
469{
470    QStringList fileTypes;
471    for (const QString &filter : *mSelectedNameFilter) {
472        if (!filter.startsWith(QLatin1String("*.")))
473            continue;
474
475        if (filter.contains(QLatin1Char('?')))
476            continue;
477
478        if (filter.count(QLatin1Char('*')) != 1)
479            continue;
480
481        auto extensions = filter.split('.', Qt::SkipEmptyParts);
482        fileTypes += extensions.last();
483
484        // Explicitly show extensions if we detect a filter
485        // that has a multi-part extension. This prevents
486        // confusing situations where the user clicks e.g.
487        // 'foo.tar.gz' and 'foo.tar' is populated in the
488        // file name box, but when then clicking save macOS
489        // will warn that the file needs to end in .gz,
490        // due to thinking the user tried to save the file
491        // as a 'tar' file instead. Unfortunately this
492        // property can only be set before the panel is
493        // shown, so it will not have any effect when
494        // swithcing filters in an already opened dialog.
495        if (extensions.size() > 2)
496            mSavePanel.extensionHidden = NO;
497    }
498
499    mSavePanel.allowedFileTypes = fileTypes.isEmpty() ? nil
500        : qt_mac_QStringListToNSMutableArray(fileTypes);
501}
502
503- (QString)removeExtensions:(const QString &)filter
504{
505    QRegularExpression regExp(QString::fromLatin1(QPlatformFileDialogHelper::filterRegExp));
506    QRegularExpressionMatch match = regExp.match(filter);
507    if (match.hasMatch())
508        return match.captured(1).trimmed();
509    return filter;
510}
511
512- (void)createTextField
513{
514    NSRect textRect = { { 0.0, 3.0 }, { 100.0, 25.0 } };
515    mTextField = [[NSTextField alloc] initWithFrame:textRect];
516    [[mTextField cell] setFont:[NSFont systemFontOfSize:
517            [NSFont systemFontSizeForControlSize:NSControlSizeRegular]]];
518    [mTextField setAlignment:NSTextAlignmentRight];
519    [mTextField setEditable:false];
520    [mTextField setSelectable:false];
521    [mTextField setBordered:false];
522    [mTextField setDrawsBackground:false];
523    if (mOptions->isLabelExplicitlySet(QFileDialogOptions::FileType))
524        [mTextField setStringValue:[self strip:mOptions->labelText(QFileDialogOptions::FileType)]];
525}
526
527- (void)createPopUpButton:(const QString &)selectedFilter hideDetails:(BOOL)hideDetails
528{
529    NSRect popUpRect = { { 100.0, 5.0 }, { 250.0, 25.0 } };
530    mPopUpButton = [[NSPopUpButton alloc] initWithFrame:popUpRect pullsDown:NO];
531    [mPopUpButton setTarget:self];
532    [mPopUpButton setAction:@selector(filterChanged:)];
533
534    if (mNameFilterDropDownList->size() > 0) {
535        int filterToUse = -1;
536        for (int i=0; i<mNameFilterDropDownList->size(); ++i) {
537            QString currentFilter = mNameFilterDropDownList->at(i);
538            if (selectedFilter == currentFilter ||
539                (filterToUse == -1 && currentFilter.startsWith(selectedFilter)))
540                filterToUse = i;
541            QString filter = hideDetails ? [self removeExtensions:currentFilter] : currentFilter;
542            [mPopUpButton addItemWithTitle:filter.toNSString()];
543        }
544        if (filterToUse != -1)
545            [mPopUpButton selectItemAtIndex:filterToUse];
546    }
547}
548
549- (QStringList) findStrippedFilterWithVisualFilterName:(QString)name
550{
551    for (int i=0; i<mNameFilterDropDownList->size(); ++i) {
552        if (mNameFilterDropDownList->at(i).startsWith(name))
553            return QPlatformFileDialogHelper::cleanFilterList(mNameFilterDropDownList->at(i));
554    }
555    return QStringList();
556}
557
558- (void)createAccessory
559{
560    NSRect accessoryRect = { { 0.0, 0.0 }, { 450.0, 33.0 } };
561    mAccessoryView = [[NSView alloc] initWithFrame:accessoryRect];
562    [mAccessoryView addSubview:mTextField];
563    [mAccessoryView addSubview:mPopUpButton];
564}
565
566@end
567
568QT_BEGIN_NAMESPACE
569
570QCocoaFileDialogHelper::QCocoaFileDialogHelper()
571    : mDelegate(nil)
572{
573}
574
575QCocoaFileDialogHelper::~QCocoaFileDialogHelper()
576{
577    if (!mDelegate)
578        return;
579    QMacAutoReleasePool pool;
580    [mDelegate release];
581    mDelegate = nil;
582}
583
584void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_selectionChanged(const QString &newPath)
585{
586    emit currentChanged(QUrl::fromLocalFile(newPath));
587}
588
589void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_panelClosed(bool accepted)
590{
591    if (accepted) {
592        emit accept();
593    } else {
594        emit reject();
595    }
596}
597
598void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_directoryEntered(const QString &newDir)
599{
600    // ### fixme: priv->setLastVisitedDirectory(newDir);
601    emit directoryEntered(QUrl::fromLocalFile(newDir));
602}
603
604void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_filterSelected(int menuIndex)
605{
606    const QStringList filters = options()->nameFilters();
607    emit filterSelected(menuIndex >= 0 && menuIndex < filters.size() ? filters.at(menuIndex) : QString());
608}
609
610void QCocoaFileDialogHelper::setDirectory(const QUrl &directory)
611{
612    if (mDelegate)
613        [mDelegate->mSavePanel setDirectoryURL:[NSURL fileURLWithPath:directory.toLocalFile().toNSString()]];
614    else
615        mDir = directory;
616}
617
618QUrl QCocoaFileDialogHelper::directory() const
619{
620    if (mDelegate) {
621        QString path = QString::fromNSString([[mDelegate->mSavePanel directoryURL] path]).normalized(QString::NormalizationForm_C);
622        return QUrl::fromLocalFile(path);
623    }
624    return mDir;
625}
626
627void QCocoaFileDialogHelper::selectFile(const QUrl &filename)
628{
629    QString filePath = filename.toLocalFile();
630    if (QDir::isRelativePath(filePath))
631        filePath = QFileInfo(directory().toLocalFile(), filePath).filePath();
632
633    // There seems to no way to select a file once the dialog is running.
634    // So do the next best thing, set the file's directory:
635    setDirectory(QFileInfo(filePath).absolutePath());
636}
637
638QList<QUrl> QCocoaFileDialogHelper::selectedFiles() const
639{
640    if (mDelegate)
641        return [mDelegate selectedFiles];
642    return QList<QUrl>();
643}
644
645void QCocoaFileDialogHelper::setFilter()
646{
647    if (!mDelegate)
648        return;
649    const SharedPointerFileDialogOptions &opts = options();
650    [mDelegate->mSavePanel setTitle:opts->windowTitle().toNSString()];
651    if (opts->isLabelExplicitlySet(QFileDialogOptions::Accept))
652        [mDelegate->mSavePanel setPrompt:[mDelegate strip:opts->labelText(QFileDialogOptions::Accept)]];
653    if (opts->isLabelExplicitlySet(QFileDialogOptions::FileName))
654        [mDelegate->mSavePanel setNameFieldLabel:[mDelegate strip:opts->labelText(QFileDialogOptions::FileName)]];
655
656    [mDelegate updateProperties];
657}
658
659void QCocoaFileDialogHelper::selectNameFilter(const QString &filter)
660{
661    if (!options())
662        return;
663    const int index = options()->nameFilters().indexOf(filter);
664    if (index != -1) {
665        if (!mDelegate) {
666            options()->setInitiallySelectedNameFilter(filter);
667            return;
668        }
669        [mDelegate->mPopUpButton selectItemAtIndex:index];
670        [mDelegate filterChanged:nil];
671    }
672}
673
674QString QCocoaFileDialogHelper::selectedNameFilter() const
675{
676    if (!mDelegate)
677        return options()->initiallySelectedNameFilter();
678    int index = [mDelegate->mPopUpButton indexOfSelectedItem];
679    if (index >= options()->nameFilters().count())
680        return QString();
681    return index != -1 ? options()->nameFilters().at(index) : QString();
682}
683
684void QCocoaFileDialogHelper::hide()
685{
686    hideCocoaFilePanel();
687}
688
689bool QCocoaFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
690{
691//    Q_Q(QFileDialog);
692    if (windowFlags & Qt::WindowStaysOnTopHint) {
693        // The native file dialog tries all it can to stay
694        // on the NSModalPanel level. And it might also show
695        // its own "create directory" dialog that we cannot control.
696        // So we need to use the non-native version in this case...
697        return false;
698    }
699
700    return showCocoaFilePanel(windowModality, parent);
701}
702
703void QCocoaFileDialogHelper::createNSOpenSavePanelDelegate()
704{
705    QMacAutoReleasePool pool;
706
707    const SharedPointerFileDialogOptions &opts = options();
708    const QList<QUrl> selectedFiles = opts->initiallySelectedFiles();
709    const QUrl directory = mDir.isEmpty() ? opts->initialDirectory() : mDir;
710    const bool selectDir = selectedFiles.isEmpty();
711    QString selection(selectDir ? directory.toLocalFile() : selectedFiles.front().toLocalFile());
712    QNSOpenSavePanelDelegate *delegate = [[QNSOpenSavePanelDelegate alloc]
713        initWithAcceptMode:
714            selection
715            options:opts
716            helper:this];
717
718    [static_cast<QNSOpenSavePanelDelegate *>(mDelegate) release];
719    mDelegate = delegate;
720}
721
722bool QCocoaFileDialogHelper::showCocoaFilePanel(Qt::WindowModality windowModality, QWindow *parent)
723{
724    createNSOpenSavePanelDelegate();
725    if (!mDelegate)
726        return false;
727    if (windowModality == Qt::NonModal)
728        [mDelegate showModelessPanel];
729    else if (windowModality == Qt::WindowModal && parent)
730        [mDelegate showWindowModalSheet:parent];
731    // no need to show a Qt::ApplicationModal dialog here, since it will be done in _q_platformRunNativeAppModalPanel()
732    return true;
733}
734
735bool QCocoaFileDialogHelper::hideCocoaFilePanel()
736{
737    if (!mDelegate){
738        // Nothing to do. We return false to leave the question
739        // open regarding whether or not to go native:
740        return false;
741    } else {
742        [mDelegate closePanel];
743        // Even when we hide it, we are still using a
744        // native dialog, so return true:
745        return true;
746    }
747}
748
749void QCocoaFileDialogHelper::exec()
750{
751    // Note: If NSApp is not running (which is the case if e.g a top-most
752    // QEventLoop has been interrupted, and the second-most event loop has not
753    // yet been reactivated (regardless if [NSApp run] is still on the stack)),
754    // showing a native modal dialog will fail.
755    QMacAutoReleasePool pool;
756    if ([mDelegate runApplicationModalPanel])
757        emit accept();
758    else
759        emit reject();
760
761}
762
763bool QCocoaFileDialogHelper::defaultNameFilterDisables() const
764{
765    return true;
766}
767
768QT_END_NAMESPACE
769