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