macCocoaDialogs.mm 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. //-----------------------------------------------------------------------------
  2. // Copyright (c) 2012 GarageGames, LLC
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to
  6. // deal in the Software without restriction, including without limitation the
  7. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  8. // sell copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  20. // IN THE SOFTWARE.
  21. //-----------------------------------------------------------------------------
  22. // Get our GL header included before Apple's
  23. #include "platformMac/platformMacCarb.h"
  24. // Don't include Apple's
  25. #define __gl_h_
  26. #include "platform/tmm_off.h"
  27. #include <Cocoa/Cocoa.h>
  28. #include "platform/tmm_on.h"
  29. #include "console/simBase.h"
  30. #include "platform/nativeDialogs/fileDialog.h"
  31. #include "platform/threads/mutex.h"
  32. #include "core/util/safeDelete.h"
  33. #include "math/mMath.h"
  34. #include "core/strings/unicode.h"
  35. #include "console/consoleTypes.h"
  36. #include "platform/threads/thread.h"
  37. #include "platform/threads/semaphore.h"
  38. class FileDialogOpaqueData
  39. {
  40. public:
  41. Semaphore *sem;
  42. FileDialogOpaqueData() { sem = new Semaphore(0); }
  43. ~FileDialogOpaqueData() { delete sem; }
  44. };
  45. class FileDialogFileExtList
  46. {
  47. public:
  48. Vector<UTF8*> list;
  49. UTF8* data;
  50. FileDialogFileExtList(const char* exts) { data = dStrdup(exts); }
  51. ~FileDialogFileExtList() { SAFE_DELETE(data); }
  52. };
  53. class FileDialogFileTypeList
  54. {
  55. public:
  56. UTF8* filterData;
  57. Vector<UTF8*> names;
  58. Vector<FileDialogFileExtList*> exts;
  59. bool any;
  60. FileDialogFileTypeList(const char* filter) { filterData = dStrdup(filter); any = false;}
  61. ~FileDialogFileTypeList()
  62. {
  63. SAFE_DELETE(filterData);
  64. for(U32 i = 0; i < exts.size(); i++)
  65. delete exts[i];
  66. }
  67. };
  68. #undef new
  69. //-----------------------------------------------------------------------------
  70. // PlatformFileDlgData Implementation
  71. //-----------------------------------------------------------------------------
  72. FileDialogData::FileDialogData()
  73. {
  74. // Default Path
  75. //
  76. // Try to provide consistent experience by recalling the last file path
  77. // - else
  78. // Default to Working Directory if last path is not set or is invalid
  79. mDefaultPath = StringTable->insert( Con::getVariable("Tools::FileDialogs::LastFilePath") );
  80. if( mDefaultPath == StringTable->lookup("") || !Platform::isDirectory( mDefaultPath ) )
  81. mDefaultPath = Platform::getCurrentDirectory();
  82. mDefaultFile = StringTable->insert("");
  83. mFilters = StringTable->insert("");
  84. mFile = StringTable->insert("");
  85. mTitle = StringTable->insert("");
  86. mStyle = 0;
  87. mOpaqueData = new FileDialogOpaqueData();
  88. }
  89. FileDialogData::~FileDialogData()
  90. {
  91. delete mOpaqueData;
  92. }
  93. //-----------------------------------------------------------------------------
  94. // FileDialog Implementation
  95. //-----------------------------------------------------------------------------
  96. IMPLEMENT_CONOBJECT(FileDialog);
  97. FileDialog::FileDialog() : mData()
  98. {
  99. // Default to File Must Exist Open Dialog style
  100. mData.mStyle = FileDialogData::FDS_OPEN | FileDialogData::FDS_MUSTEXIST;
  101. mChangePath = false;
  102. }
  103. FileDialog::~FileDialog()
  104. {
  105. }
  106. void FileDialog::initPersistFields()
  107. {
  108. // why is this stuff buried in another class?
  109. addProtectedField("DefaultPath", TypeString, Offset(mData.mDefaultPath, FileDialog), &setDefaultPath, &defaultProtectedGetFn, "Default Path when Dialog is shown");
  110. addProtectedField("DefaultFile", TypeString, Offset(mData.mDefaultFile, FileDialog), &setDefaultFile, &defaultProtectedGetFn, "Default File when Dialog is shown");
  111. addProtectedField("FileName", TypeString, Offset(mData.mFile, FileDialog), &setFile, &defaultProtectedGetFn, "Default File when Dialog is shown");
  112. addProtectedField("Filters", TypeString, Offset(mData.mFilters, FileDialog), &setFilters, &defaultProtectedGetFn, "Default File when Dialog is shown");
  113. addField("Title", TypeString, Offset(mData.mTitle, FileDialog), "Default File when Dialog is shown");
  114. addProtectedField("ChangePath", TypeBool, Offset(mChangePath, FileDialog), &setChangePath, &getChangePath, "True/False whether to set the working directory to the directory returned by the dialog" );
  115. Parent::initPersistFields();
  116. }
  117. static FileDialogFileExtList* _MacCarbGetFileExtensionsFromString(const char* filter)
  118. {
  119. FileDialogFileExtList* list = new FileDialogFileExtList(filter);
  120. char* token = list->data;
  121. char* place = list->data;
  122. for( ; *place; place++)
  123. {
  124. if(*place != ';')
  125. continue;
  126. *place = '\0';
  127. list->list.push_back(token);
  128. ++place;
  129. token = place;
  130. }
  131. // last token
  132. list->list.push_back(token);
  133. return list;
  134. }
  135. static FileDialogFileTypeList* _MacCarbGetFileTypesFromString(const char* filter)
  136. {
  137. FileDialogFileTypeList &list = *(new FileDialogFileTypeList(filter));
  138. char* token = list.filterData;
  139. char* place = list.filterData;
  140. // scan the filter list until we hit a null.
  141. // when we see the separator '|', replace it with a null, and save the token
  142. // format is description|extension|description|extension
  143. bool isDesc = true;
  144. for( ; *place; place++)
  145. {
  146. if(*place != '|')
  147. continue;
  148. *place = '\0';
  149. if(isDesc)
  150. list.names.push_back(token);
  151. else
  152. {
  153. // detect *.*
  154. if(dStrstr((const char*)token, "*.*"))
  155. list.any = true;
  156. list.exts.push_back(_MacCarbGetFileExtensionsFromString(token));
  157. }
  158. isDesc = !isDesc;
  159. ++place;
  160. token = place;
  161. }
  162. list.exts.push_back(_MacCarbGetFileExtensionsFromString(token));
  163. return &list;
  164. }
  165. static NSArray* _MacCocoaCreateAndRunSavePanel(FileDialogData &mData)
  166. {
  167. NSSavePanel* panel = [NSSavePanel savePanel];
  168. // User freedom niceties
  169. [panel setCanCreateDirectories:YES];
  170. [panel setCanSelectHiddenExtension:YES];
  171. [panel setTreatsFilePackagesAsDirectories:YES];
  172. NSString *initialFile = [[NSString stringWithUTF8String:mData.mDefaultFile] lastPathComponent];
  173. // we only use mDefaultDir if mDefault path is not set.
  174. NSString *dir;
  175. if(dStrlen(mData.mDefaultPath) < 1)
  176. dir = [[NSString stringWithUTF8String:mData.mDefaultFile] stringByDeletingLastPathComponent];
  177. else
  178. dir = [NSString stringWithUTF8String: mData.mDefaultPath];
  179. [panel setDirectory:dir];
  180. // todo: move file type handling to an accessory view.
  181. // parse file types
  182. FileDialogFileTypeList *fTypes = _MacCarbGetFileTypesFromString(mData.mFilters);
  183. // fill an array with the possible file types
  184. NSMutableArray* types = [NSMutableArray arrayWithCapacity:10];
  185. for(U32 i = 0; i < fTypes->exts.size(); i++)
  186. {
  187. for(U32 j = 0; j < fTypes->exts[i]->list.size(); j++)
  188. {
  189. char* ext = fTypes->exts[i]->list[j];
  190. if(ext)
  191. {
  192. if(dStrlen(ext) == 0)
  193. continue;
  194. if(dStrncmp(ext, "*.*", 3) == 0)
  195. continue;
  196. if(dStrncmp(ext, "*.", 2) == 0)
  197. ext+=2;
  198. [types addObject:[NSString stringWithUTF8String:ext]];
  199. }
  200. }
  201. }
  202. if([types count] > 0)
  203. [panel setAllowedFileTypes:types];
  204. // if any file type was *.*, user may select any file type.
  205. [panel setAllowsOtherFileTypes:fTypes->any];
  206. //---------------------------------------------------------------------------
  207. // Display the panel, enter a modal loop. This blocks.
  208. //---------------------------------------------------------------------------
  209. U32 button = [panel runModalForDirectory:dir file:initialFile];
  210. // return the file name
  211. NSMutableArray *array = [NSMutableArray arrayWithCapacity:10];
  212. if(button != NSFileHandlingPanelCancelButton)
  213. [array addObject:[panel filename]];
  214. delete fTypes;
  215. return array;
  216. // TODO: paxorr: show as sheet
  217. // crashes when we try to display the window as a sheet. Not sure why.
  218. // the sheet is instantly dismissed, and crashes as it's dismissing itself.
  219. // here's the code snippet to get an nswindow from our carbon WindowRef
  220. //NSWindow *nsAppWindow = [[NSWindow alloc] initWithWindowRef:platState.appWindow];
  221. }
  222. NSArray* _MacCocoaCreateAndRunOpenPanel(FileDialogData &mData)
  223. {
  224. NSOpenPanel* panel = [NSOpenPanel openPanel];
  225. // User freedom niceties
  226. [panel setCanCreateDirectories:YES];
  227. [panel setCanSelectHiddenExtension:YES];
  228. [panel setTreatsFilePackagesAsDirectories:YES];
  229. [panel setAllowsMultipleSelection:(mData.mStyle & FileDialogData::FDS_MULTIPLEFILES)];
  230. //
  231. bool chooseDir = (mData.mStyle & FileDialogData::FDS_BROWSEFOLDER);
  232. [panel setCanChooseFiles: !chooseDir ];
  233. [panel setCanChooseDirectories: chooseDir ];
  234. if(chooseDir)
  235. {
  236. [panel setPrompt:@"Choose"];
  237. [panel setTitle:@"Choose Folder"];
  238. }
  239. NSString *initialFile = [[NSString stringWithUTF8String:mData.mDefaultFile] lastPathComponent];
  240. // we only use mDefaultDir if mDefault path is not set.
  241. NSString *dir;
  242. if(dStrlen(mData.mDefaultPath) < 1)
  243. dir = [[NSString stringWithUTF8String:mData.mDefaultFile] stringByDeletingLastPathComponent];
  244. else
  245. dir = [NSString stringWithUTF8String: mData.mDefaultPath];
  246. [panel setDirectory:dir];
  247. // todo: move file type handling to an accessory view.
  248. // parse file types
  249. FileDialogFileTypeList *fTypes = _MacCarbGetFileTypesFromString(mData.mFilters);
  250. // fill an array with the possible file types
  251. NSMutableArray* types = [NSMutableArray arrayWithCapacity:10];
  252. for(U32 i = 0; i < fTypes->exts.size(); i++)
  253. {
  254. for(U32 j = 0; j < fTypes->exts[i]->list.size(); j++)
  255. {
  256. char* ext = fTypes->exts[i]->list[j];
  257. if(ext)
  258. {
  259. if(dStrncmp(ext, "*.", 2) == 0)
  260. ext+=2;
  261. [types addObject:[NSString stringWithUTF8String:ext]];
  262. }
  263. }
  264. }
  265. if([types count] > 0)
  266. [panel setAllowedFileTypes:types];
  267. // if any file type was *.*, user may select any file type.
  268. [panel setAllowsOtherFileTypes:fTypes->any];
  269. //---------------------------------------------------------------------------
  270. // Display the panel, enter a modal loop. This blocks.
  271. //---------------------------------------------------------------------------
  272. U32 button = [panel runModalForDirectory:dir file:initialFile ];
  273. // return the file name
  274. NSMutableArray *array = [NSMutableArray arrayWithCapacity:10];
  275. if(button != NSFileHandlingPanelCancelButton)
  276. [array addObject:[panel filename]];
  277. delete fTypes;
  278. return array;
  279. }
  280. void MacCarbShowDialog(void* dialog)
  281. {
  282. FileDialog* d = static_cast<FileDialog*>(dialog);
  283. d->Execute();
  284. }
  285. //
  286. // Execute Method
  287. //
  288. bool FileDialog::Execute()
  289. {
  290. // if(! ThreadManager::isCurrentThread(platState.firstThreadId))
  291. // {
  292. // MacCarbSendTorqueEventToMain(kEventTorqueModalDialog,this);
  293. // mData.mOpaqueData->sem->acquire();
  294. // return;
  295. // }
  296. NSArray* nsFileArray;
  297. if(mData.mStyle & FileDialogData::FDS_OPEN)
  298. nsFileArray = _MacCocoaCreateAndRunOpenPanel(mData);
  299. else if(mData.mStyle & FileDialogData::FDS_SAVE)
  300. nsFileArray = _MacCocoaCreateAndRunSavePanel(mData);
  301. else
  302. {
  303. Con::errorf("Bad File Dialog Setup.");
  304. mData.mOpaqueData->sem->release();
  305. return false;
  306. }
  307. if([nsFileArray count] == 0)
  308. return false;
  309. if(! (mData.mStyle & FileDialogData::FDS_MULTIPLEFILES) && [nsFileArray count] >= 1)
  310. {
  311. const UTF8* f = [(NSString*)[nsFileArray objectAtIndex:0] UTF8String];
  312. mData.mFile = StringTable->insert(f);
  313. }
  314. else
  315. {
  316. for(U32 i = 0; i < [nsFileArray count]; i++)
  317. {
  318. const UTF8* f = [(NSString*)[nsFileArray objectAtIndex:i] UTF8String];
  319. setDataField(StringTable->insert("files"), Con::getIntArg(i), StringTable->insert(f));
  320. }
  321. setDataField(StringTable->insert("fileCount"), NULL, Con::getIntArg([nsFileArray count]));
  322. }
  323. mData.mOpaqueData->sem->release();
  324. return true;
  325. }
  326. ConsoleMethod( FileDialog, Execute, bool, 2, 2, "%fileDialog.Execute();" )
  327. {
  328. return object->Execute();
  329. }
  330. //-----------------------------------------------------------------------------
  331. // Dialog Filters
  332. //-----------------------------------------------------------------------------
  333. bool FileDialog::setFilters(void* obj, const char* index, const char* data)
  334. {
  335. // Will do validate on write at some point.
  336. if( !data )
  337. return true;
  338. return true;
  339. };
  340. //-----------------------------------------------------------------------------
  341. // Default Path Property - String Validated on Write
  342. //-----------------------------------------------------------------------------
  343. bool FileDialog::setDefaultPath(void* obj, const char* index, const char* data)
  344. {
  345. if( !data )
  346. return true;
  347. return true;
  348. };
  349. //-----------------------------------------------------------------------------
  350. // Default File Property - String Validated on Write
  351. //-----------------------------------------------------------------------------
  352. bool FileDialog::setDefaultFile(void* obj, const char* index, const char* data)
  353. {
  354. if( !data )
  355. return true;
  356. return true;
  357. };
  358. //-----------------------------------------------------------------------------
  359. // ChangePath Property - Change working path on successful file selection
  360. //-----------------------------------------------------------------------------
  361. bool FileDialog::setChangePath(void* obj, const char* index, const char* data)
  362. {
  363. bool bChangePath = dAtob( data );
  364. FileDialog *pDlg = static_cast<FileDialog*>( obj );
  365. if( bChangePath )
  366. pDlg->mData.mStyle |= FileDialogData::FDS_CHANGEPATH;
  367. else
  368. pDlg->mData.mStyle &= ~FileDialogData::FDS_CHANGEPATH;
  369. return true;
  370. };
  371. const char* FileDialog::getChangePath(void* obj, const char* data)
  372. {
  373. FileDialog *pDlg = static_cast<FileDialog*>( obj );
  374. if( pDlg->mData.mStyle & FileDialogData::FDS_CHANGEPATH )
  375. return StringTable->insert("true");
  376. else
  377. return StringTable->insert("false");
  378. }
  379. bool FileDialog::setFile(void* obj, const char* index, const char* data)
  380. {
  381. return false;
  382. };
  383. //-----------------------------------------------------------------------------
  384. // OpenFileDialog Implementation
  385. //-----------------------------------------------------------------------------
  386. OpenFileDialog::OpenFileDialog()
  387. {
  388. // Default File Must Exist
  389. mData.mStyle = FileDialogData::FDS_OPEN | FileDialogData::FDS_MUSTEXIST;
  390. }
  391. OpenFileDialog::~OpenFileDialog()
  392. {
  393. mMustExist = true;
  394. mMultipleFiles = false;
  395. }
  396. IMPLEMENT_CONOBJECT(OpenFileDialog);
  397. //-----------------------------------------------------------------------------
  398. // Console Properties
  399. //-----------------------------------------------------------------------------
  400. void OpenFileDialog::initPersistFields()
  401. {
  402. addProtectedField("MustExist", TypeBool, Offset(mMustExist, OpenFileDialog), &setMustExist, &getMustExist, "True/False whether the file returned must exist or not" );
  403. addProtectedField("MultipleFiles", TypeBool, Offset(mMultipleFiles, OpenFileDialog), &setMultipleFiles, &getMultipleFiles, "True/False whether multiple files may be selected and returned or not" );
  404. Parent::initPersistFields();
  405. }
  406. //-----------------------------------------------------------------------------
  407. // File Must Exist - Boolean
  408. //-----------------------------------------------------------------------------
  409. bool OpenFileDialog::setMustExist(void* obj, const char* index, const char* data)
  410. {
  411. bool bMustExist = dAtob( data );
  412. OpenFileDialog *pDlg = static_cast<OpenFileDialog*>( obj );
  413. if( bMustExist )
  414. pDlg->mData.mStyle |= FileDialogData::FDS_MUSTEXIST;
  415. else
  416. pDlg->mData.mStyle &= ~FileDialogData::FDS_MUSTEXIST;
  417. return true;
  418. };
  419. const char* OpenFileDialog::getMustExist(void* obj, const char* data)
  420. {
  421. OpenFileDialog *pDlg = static_cast<OpenFileDialog*>( obj );
  422. if( pDlg->mData.mStyle & FileDialogData::FDS_MUSTEXIST )
  423. return StringTable->insert("true");
  424. else
  425. return StringTable->insert("false");
  426. }
  427. //-----------------------------------------------------------------------------
  428. // Can Select Multiple Files - Boolean
  429. //-----------------------------------------------------------------------------
  430. bool OpenFileDialog::setMultipleFiles(void* obj, const char* index, const char* data)
  431. {
  432. bool bMustExist = dAtob( data );
  433. OpenFileDialog *pDlg = static_cast<OpenFileDialog*>( obj );
  434. if( bMustExist )
  435. pDlg->mData.mStyle |= FileDialogData::FDS_MULTIPLEFILES;
  436. else
  437. pDlg->mData.mStyle &= ~FileDialogData::FDS_MULTIPLEFILES;
  438. return true;
  439. };
  440. const char* OpenFileDialog::getMultipleFiles(void* obj, const char* data)
  441. {
  442. OpenFileDialog *pDlg = static_cast<OpenFileDialog*>( obj );
  443. if( pDlg->mData.mStyle & FileDialogData::FDS_MULTIPLEFILES )
  444. return StringTable->insert("true");
  445. else
  446. return StringTable->insert("false");
  447. }
  448. //-----------------------------------------------------------------------------
  449. // SaveFileDialog Implementation
  450. //-----------------------------------------------------------------------------
  451. SaveFileDialog::SaveFileDialog()
  452. {
  453. // Default File Must Exist
  454. mData.mStyle = FileDialogData::FDS_SAVE | FileDialogData::FDS_OVERWRITEPROMPT;
  455. mOverwritePrompt = true;
  456. }
  457. SaveFileDialog::~SaveFileDialog()
  458. {
  459. }
  460. IMPLEMENT_CONOBJECT(SaveFileDialog);
  461. //-----------------------------------------------------------------------------
  462. // Console Properties
  463. //-----------------------------------------------------------------------------
  464. void SaveFileDialog::initPersistFields()
  465. {
  466. addProtectedField("OverwritePrompt", TypeBool, Offset(mOverwritePrompt, SaveFileDialog), &setOverwritePrompt, &getOverwritePrompt, "True/False whether the dialog should prompt before accepting an existing file name" );
  467. Parent::initPersistFields();
  468. }
  469. //-----------------------------------------------------------------------------
  470. // Prompt on Overwrite - Boolean
  471. //-----------------------------------------------------------------------------
  472. bool SaveFileDialog::setOverwritePrompt(void* obj, const char* index, const char* data)
  473. {
  474. bool bOverwrite = dAtob( data );
  475. SaveFileDialog *pDlg = static_cast<SaveFileDialog*>( obj );
  476. if( bOverwrite )
  477. pDlg->mData.mStyle |= FileDialogData::FDS_OVERWRITEPROMPT;
  478. else
  479. pDlg->mData.mStyle &= ~FileDialogData::FDS_OVERWRITEPROMPT;
  480. return true;
  481. };
  482. const char* SaveFileDialog::getOverwritePrompt(void* obj, const char* data)
  483. {
  484. SaveFileDialog *pDlg = static_cast<SaveFileDialog*>( obj );
  485. if( pDlg->mData.mStyle & FileDialogData::FDS_OVERWRITEPROMPT )
  486. return StringTable->insert("true");
  487. else
  488. return StringTable->insert("false");
  489. }
  490. //-----------------------------------------------------------------------------
  491. // OpenFolderDialog Implementation
  492. //-----------------------------------------------------------------------------
  493. OpenFolderDialog::OpenFolderDialog()
  494. {
  495. mData.mStyle = FileDialogData::FDS_OPEN | FileDialogData::FDS_OVERWRITEPROMPT | FileDialogData::FDS_BROWSEFOLDER;
  496. mMustExistInDir = "";
  497. }
  498. IMPLEMENT_CONOBJECT(OpenFolderDialog);
  499. void OpenFolderDialog::initPersistFields()
  500. {
  501. addField("fileMustExist", TypeFilename, Offset(mMustExistInDir, OpenFolderDialog), "File that must in selected folder for it to be valid");
  502. Parent::initPersistFields();
  503. }