1 // Written by Christopher E. Miller 2 // See the included license.txt for copyright and license details. 3 4 5 6 module dfl.filedialog; 7 8 private import dfl.internal.dlib; 9 10 private import dfl.control, dfl.internal.winapi, dfl.base, dfl.drawing; 11 private import dfl.application, dfl.commondialog, dfl.event, dfl.internal.utf; 12 13 14 15 abstract class FileDialog: CommonDialog { // docmain 16 private this() { 17 Application.ppin(cast(void*)this); 18 19 ofn.lStructSize = ofn.sizeof; 20 ofn.lCustData = cast(typeof(ofn.lCustData))cast(void*)this; 21 ofn.Flags = INIT_FLAGS; 22 ofn.nFilterIndex = INIT_FILTER_INDEX; 23 initInstance(); 24 ofn.lpfnHook = cast(typeof(ofn.lpfnHook))&ofnHookProc; 25 } 26 27 28 override DialogResult showDialog() { 29 return runDialog(GetActiveWindow()) ? 30 DialogResult.OK : DialogResult.CANCEL; 31 } 32 33 override DialogResult showDialog(IWindow owner) { 34 return runDialog(owner ? owner.handle : GetActiveWindow()) ? 35 DialogResult.OK : DialogResult.CANCEL; 36 } 37 38 39 override void reset() { 40 ofn.Flags = INIT_FLAGS; 41 ofn.lpstrFilter = null; 42 ofn.nFilterIndex = INIT_FILTER_INDEX; 43 ofn.lpstrDefExt = null; 44 _defext = null; 45 _fileNames = null; 46 needRebuildFiles = false; 47 _filter = null; 48 ofn.lpstrInitialDir = null; 49 _initDir = null; 50 ofn.lpstrTitle = null; 51 _title = null; 52 initInstance(); 53 } 54 55 56 private void initInstance() { 57 //ofn.hInstance = ?; // Should this be initialized? 58 } 59 60 61 /+ 62 final @property void addExtension(bool byes) { // setter 63 addext = byes; 64 } 65 66 67 final @property bool addExtension() { // getter 68 return addext; 69 } 70 +/ 71 72 73 74 @property void checkFileExists(bool byes) { // setter 75 if(byes) { 76 ofn.Flags |= OFN_FILEMUSTEXIST; 77 } else { 78 ofn.Flags &= ~OFN_FILEMUSTEXIST; 79 } 80 } 81 82 /// ditto 83 @property bool checkFileExists() { // getter 84 return (ofn.Flags & OFN_FILEMUSTEXIST) != 0; 85 } 86 87 88 89 final @property void checkPathExists(bool byes) { // setter 90 if(byes) { 91 ofn.Flags |= OFN_PATHMUSTEXIST; 92 } else { 93 ofn.Flags &= ~OFN_PATHMUSTEXIST; 94 } 95 } 96 97 /// ditto 98 final @property bool checkPathExists() { // getter 99 return (ofn.Flags & OFN_PATHMUSTEXIST) != 0; 100 } 101 102 103 104 final @property void defaultExt(Dstring ext) { // setter 105 if(!ext.length) { 106 ofn.lpstrDefExt = null; 107 _defext = null; 108 } else { 109 if(ext.length && ext[0] == '.') { 110 ext = ext[1 .. ext.length]; 111 } 112 113 if(dfl.internal.utf.useUnicode) { 114 ofnw.lpstrDefExt = dfl.internal.utf.toUnicodez(ext); 115 } else { 116 ofna.lpstrDefExt = dfl.internal.utf.toAnsiz(ext); 117 } 118 _defext = ext; 119 } 120 } 121 122 /// ditto 123 final @property Dstring defaultExt() { // getter 124 return _defext; 125 } 126 127 final @property void dereferenceLinks(bool byes) { // setter 128 if(byes) { 129 ofn.Flags &= ~OFN_NODEREFERENCELINKS; 130 } else { 131 ofn.Flags |= OFN_NODEREFERENCELINKS; 132 } 133 } 134 135 /// ditto 136 final @property bool dereferenceLinks() { // getter 137 return (ofn.Flags & OFN_NODEREFERENCELINKS) == 0; 138 } 139 140 final @property void fileName(Dstring fn) { // setter 141 // TODO: check if correct implementation. 142 143 if(fn.length > MAX_PATH) { 144 throw new DflException("Invalid file name"); 145 } 146 147 if(fileNames.length) { 148 _fileNames = (&fn)[0 .. 1] ~ _fileNames[1 .. _fileNames.length]; 149 } else { 150 _fileNames = new Dstring[1]; 151 _fileNames[0] = fn; 152 } 153 } 154 155 /// ditto 156 final @property Dstring fileName() { // getter 157 if(fileNames.length) { 158 return fileNames[0]; 159 } 160 return null; 161 } 162 163 164 165 final @property Dstring[] fileNames() { // getter 166 if(needRebuildFiles) { 167 populateFiles(); 168 } 169 170 return _fileNames; 171 } 172 173 174 175 // The format string is like "Text files (*.txt)|*.txt|All files (*.*)|*.*". 176 final @property void filter(Dstring filterString) { // setter 177 if(!filterString.length) { 178 ofn.lpstrFilter = null; 179 _filter = null; 180 } else { 181 struct _Str { 182 union { 183 wchar[] sw; 184 char[] sa; 185 } 186 } 187 _Str str; 188 189 size_t i, starti; 190 size_t nitems = 0; 191 192 if(dfl.internal.utf.useUnicode) { 193 str.sw = new wchar[filterString.length + 2]; 194 str.sw = str.sw[0 .. 0]; 195 } else { 196 str.sa = new char[filterString.length + 2]; 197 str.sa = str.sa[0 .. 0]; 198 } 199 200 201 for(i = starti = 0; i != filterString.length; i++) { 202 switch(filterString[i]) { 203 case '|': 204 if(starti == i) { 205 goto bad_filter; 206 } 207 208 if(dfl.internal.utf.useUnicode) { 209 str.sw ~= dfl.internal.utf.toUnicode(filterString[starti .. i]); 210 str.sw ~= "\0"w; 211 } else { 212 str.sa ~= dfl.internal.utf.unsafeAnsi(filterString[starti .. i]); 213 str.sa ~= "\0"; 214 } 215 216 starti = i + 1; 217 nitems++; 218 break; 219 220 case 0: 221 case '\r', '\n': 222 goto bad_filter; 223 224 default: 225 } 226 } 227 if(starti == i || !(nitems % 2)) { 228 goto bad_filter; 229 } 230 if(dfl.internal.utf.useUnicode) { 231 str.sw ~= dfl.internal.utf.toUnicode(filterString[starti .. i]); 232 str.sw ~= "\0\0"w; 233 234 ofnw.lpstrFilter = str.sw.ptr; 235 } else { 236 str.sa ~= dfl.internal.utf.unsafeAnsi(filterString[starti .. i]); 237 str.sa ~= "\0\0"; 238 239 ofna.lpstrFilter = str.sa.ptr; 240 } 241 242 _filter = filterString; 243 return; 244 245 bad_filter: 246 throw new DflException("Invalid file filter string"); 247 } 248 } 249 250 /// ditto 251 final @property Dstring filter() { // getter 252 return _filter; 253 } 254 255 256 257 // Note: index is 1-based. 258 final @property void filterIndex(int index) { // setter 259 ofn.nFilterIndex = (index > 0) ? index : 1; 260 } 261 262 /// ditto 263 final @property int filterIndex() { // getter 264 return ofn.nFilterIndex; 265 } 266 267 final @property void initialDirectory(Dstring dir) { // setter 268 if(!dir.length) { 269 ofn.lpstrInitialDir = null; 270 _initDir = null; 271 } else { 272 if(dfl.internal.utf.useUnicode) { 273 ofnw.lpstrInitialDir = dfl.internal.utf.toUnicodez(dir); 274 } else { 275 ofna.lpstrInitialDir = dfl.internal.utf.toAnsiz(dir); 276 } 277 _initDir = dir; 278 } 279 } 280 281 /// ditto 282 final @property Dstring initialDirectory() { // getter 283 return _initDir; 284 } 285 286 // Should be instance(), but conflicts with D's old keyword. 287 protected @property void inst(HINSTANCE hinst) { // setter 288 ofn.hInstance = hinst; 289 } 290 291 /// ditto 292 protected @property HINSTANCE inst() { // getter 293 return ofn.hInstance; 294 } 295 296 protected @property DWORD options() { // getter 297 return ofn.Flags; 298 } 299 300 final @property void restoreDirectory(bool byes) { // setter 301 if(byes) { 302 ofn.Flags |= OFN_NOCHANGEDIR; 303 } else { 304 ofn.Flags &= ~OFN_NOCHANGEDIR; 305 } 306 } 307 308 /// ditto 309 final @property bool restoreDirectory() { // getter 310 return (ofn.Flags & OFN_NOCHANGEDIR) != 0; 311 } 312 313 final @property void showHelp(bool byes) { // setter 314 if(byes) { 315 ofn.Flags |= OFN_SHOWHELP; 316 } else { 317 ofn.Flags &= ~OFN_SHOWHELP; 318 } 319 } 320 321 /// ditto 322 final @property bool showHelp() { // getter 323 return (ofn.Flags & OFN_SHOWHELP) != 0; 324 } 325 326 final @property void title(Dstring newTitle) { // setter 327 if(!newTitle.length) { 328 ofn.lpstrTitle = null; 329 _title = null; 330 } else { 331 if(dfl.internal.utf.useUnicode) { 332 ofnw.lpstrTitle = dfl.internal.utf.toUnicodez(newTitle); 333 } else { 334 ofna.lpstrTitle = dfl.internal.utf.toAnsiz(newTitle); 335 } 336 _title = newTitle; 337 } 338 } 339 340 /// ditto 341 final @property Dstring title() { // getter 342 return _title; 343 } 344 345 final @property void validateNames(bool byes) { // setter 346 if(byes) { 347 ofn.Flags &= ~OFN_NOVALIDATE; 348 } else { 349 ofn.Flags |= OFN_NOVALIDATE; 350 } 351 } 352 353 /// ditto 354 final @property bool validateNames() { // getter 355 return(ofn.Flags & OFN_NOVALIDATE) == 0; 356 } 357 358 Event!(FileDialog, CancelEventArgs) fileOk; 359 360 361 protected: 362 363 override bool runDialog(HWND owner) { 364 assert(0); 365 } 366 367 368 void onFileOk(CancelEventArgs ea) { 369 fileOk(this, ea); 370 } 371 372 373 override LRESULT hookProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { 374 switch(msg) { 375 case WM_NOTIFY: { 376 NMHDR* nmhdr; 377 nmhdr = cast(NMHDR*)lparam; 378 switch(nmhdr.code) { 379 case CDN_FILEOK: { 380 CancelEventArgs cea; 381 cea = new CancelEventArgs; 382 onFileOk(cea); 383 if(cea.cancel) { 384 SetWindowLongA(hwnd, DWL_MSGRESULT, 1); 385 return 1; 386 } 387 } 388 break; 389 390 default: 391 //cprintf(" nmhdr.code = %d/0x%X\n", nmhdr.code, nmhdr.code); 392 } 393 } 394 break; 395 396 default: 397 } 398 399 return super.hookProc(hwnd, msg, wparam, lparam); 400 } 401 402 403 private: 404 union { 405 OPENFILENAMEW ofnw; 406 OPENFILENAMEA ofna; 407 alias ofnw ofn; 408 409 static assert(OPENFILENAMEW.sizeof == OPENFILENAMEA.sizeof); 410 static assert(OPENFILENAMEW.Flags.offsetof == OPENFILENAMEA.Flags.offsetof); 411 } 412 Dstring[] _fileNames; 413 Dstring _filter; 414 Dstring _initDir; 415 Dstring _defext; 416 Dstring _title; 417 //bool addext = true; 418 bool needRebuildFiles = false; 419 420 enum DWORD INIT_FLAGS = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_HIDEREADONLY | 421 OFN_ENABLEHOOK | OFN_ENABLESIZING; 422 enum INIT_FILTER_INDEX = 0; 423 enum FILE_BUF_LEN = 4096; // ? 12288 ? 12800 ? 424 425 426 void beginOfn(HWND owner) { 427 if(dfl.internal.utf.useUnicode) { 428 auto buf = new wchar[(ofn.Flags & OFN_ALLOWMULTISELECT) ? FILE_BUF_LEN : MAX_PATH]; 429 buf[0] = 0; 430 431 if(fileNames.length) { 432 Dwstring ts; 433 ts = dfl.internal.utf.toUnicode(_fileNames[0]); 434 buf[0 .. ts.length] = ts[]; 435 buf[ts.length] = 0; 436 } 437 438 ofnw.nMaxFile = buf.length; 439 ofnw.lpstrFile = buf.ptr; 440 } else { 441 auto buf = new char[(ofn.Flags & OFN_ALLOWMULTISELECT) ? FILE_BUF_LEN : MAX_PATH]; 442 buf[0] = 0; 443 444 if(fileNames.length) { 445 Dstring ts; 446 ts = dfl.internal.utf.unsafeAnsi(_fileNames[0]); 447 buf[0 .. ts.length] = ts[]; 448 buf[ts.length] = 0; 449 } 450 451 ofna.nMaxFile = buf.length; 452 ofna.lpstrFile = buf.ptr; 453 } 454 455 ofn.hwndOwner = owner; 456 } 457 458 459 // Populate -_fileNames- from -ofn.lpstrFile-. 460 void populateFiles() 461 in { 462 assert(ofn.lpstrFile !is null); 463 } 464 body { 465 if(ofn.Flags & OFN_ALLOWMULTISELECT) { 466 // Nonstandard reserve. 467 _fileNames = new Dstring[4]; 468 _fileNames = _fileNames[0 .. 0]; 469 470 if(dfl.internal.utf.useUnicode) { 471 wchar* startp, p; 472 p = startp = ofnw.lpstrFile; 473 for(;;) { 474 if(!*p) { 475 _fileNames ~= dfl.internal.utf.fromUnicode(startp, p - startp); // dup later. 476 477 p++; 478 if(!*p) { 479 break; 480 } 481 482 startp = p; 483 continue; 484 } 485 486 p++; 487 } 488 } else { 489 char* startp, p; 490 p = startp = ofna.lpstrFile; 491 for(;;) { 492 if(!*p) { 493 _fileNames ~= dfl.internal.utf.fromAnsi(startp, p - startp); // dup later. 494 495 p++; 496 if(!*p) { 497 break; 498 } 499 500 startp = p; 501 continue; 502 } 503 504 p++; 505 } 506 } 507 508 assert(_fileNames.length); 509 if (_fileNames.length == 1) { 510 //_fileNames[0] = _fileNames[0].dup; 511 //_fileNames[0] = _fileNames[0].idup; // Needed in D2. Doesn't work in D1. 512 _fileNames[0] = cast(Dstring)_fileNames[0].dup; // Needed in D2. 513 } else { 514 Dstring s; 515 size_t i; 516 s = _fileNames[0]; 517 518 // Not sure which of these 2 is better... 519 /+ 520 for(i = 1; i != _fileNames.length; i++) { 521 _fileNames[i - 1] = pathJoin(s, _fileNames[i]); 522 } 523 _fileNames = _fileNames[0 .. _fileNames.length - 1]; 524 +/ 525 for(i = 1; i != _fileNames.length; i++) { 526 _fileNames[i] = pathJoin(s, _fileNames[i]); 527 } 528 _fileNames = _fileNames[1 .. _fileNames.length]; 529 } 530 } else { 531 _fileNames = new Dstring[1]; 532 if(dfl.internal.utf.useUnicode) { 533 _fileNames[0] = dfl.internal.utf.fromUnicodez(ofnw.lpstrFile); 534 } else { 535 _fileNames[0] = dfl.internal.utf.fromAnsiz(ofna.lpstrFile); 536 } 537 538 /+ 539 if(addext && checkFileExists() && ofn.nFilterIndex) { 540 if(!ofn.nFileExtension || ofn.nFileExtension == _fileNames[0].length) { 541 Dstring s; 542 typeof(ofn.nFilterIndex) onidx; 543 int i; 544 Dstring[] exts; 545 546 s = _filter; 547 onidx = ofn.nFilterIndex << 1; 548 do { 549 i = charFindInString(s, '|'); 550 if(i == -1) { 551 goto no_such_filter; 552 } 553 554 s = s[i + 1 .. s.length]; 555 556 onidx--; 557 } while(onidx != 1); 558 559 i = charFindInString(s, '|'); 560 if(i != -1) { 561 s = s[0 .. i]; 562 } 563 564 exts = stringSplit(s, ";"); 565 foreach(Dstring ext; exts) { 566 cprintf("sel ext: %.*s\n", ext); 567 } 568 569 // ... 570 571 no_such_filter: ; 572 } 573 } 574 +/ 575 } 576 577 needRebuildFiles = false; 578 } 579 580 581 // Call only if the dialog succeeded. 582 void finishOfn() { 583 if(needRebuildFiles) { 584 populateFiles(); 585 } 586 587 ofn.lpstrFile = null; 588 } 589 590 591 // Call only if dialog fail or cancel. 592 void cancelOfn() { 593 needRebuildFiles = false; 594 595 ofn.lpstrFile = null; 596 _fileNames = null; 597 } 598 } 599 600 601 private extern(Windows) nothrow { 602 alias BOOL function(LPOPENFILENAMEW lpofn) GetOpenFileNameWProc; 603 alias BOOL function(LPOPENFILENAMEW lpofn) GetSaveFileNameWProc; 604 } 605 606 class OpenFileDialog: FileDialog { // docmain 607 this() { 608 super(); 609 ofn.Flags |= OFN_FILEMUSTEXIST; 610 } 611 612 613 override void reset() { 614 super.reset(); 615 ofn.Flags |= OFN_FILEMUSTEXIST; 616 } 617 618 619 620 final @property void multiselect(bool byes) { // setter 621 if(byes) { 622 ofn.Flags |= OFN_ALLOWMULTISELECT; 623 } else { 624 ofn.Flags &= ~OFN_ALLOWMULTISELECT; 625 } 626 } 627 628 /// ditto 629 final @property bool multiselect() { // getter 630 return (ofn.Flags & OFN_ALLOWMULTISELECT) != 0; 631 } 632 633 634 635 final @property void readOnlyChecked(bool byes) { // setter 636 if(byes) { 637 ofn.Flags |= OFN_READONLY; 638 } else { 639 ofn.Flags &= ~OFN_READONLY; 640 } 641 } 642 643 /// ditto 644 final @property bool readOnlyChecked() { // getter 645 return (ofn.Flags & OFN_READONLY) != 0; 646 } 647 648 649 650 final @property void showReadOnly(bool byes) { // setter 651 if(byes) { 652 ofn.Flags &= ~OFN_HIDEREADONLY; 653 } else { 654 ofn.Flags |= OFN_HIDEREADONLY; 655 } 656 } 657 658 /// ditto 659 final @property bool showReadOnly() { // getter 660 return (ofn.Flags & OFN_HIDEREADONLY) == 0; 661 } 662 663 664 private import std.stream; // TO-DO: remove this import; use dfl.internal.dlib. 665 666 667 final Stream openFile() { 668 return new File(fileName(), FileMode.In); 669 } 670 671 672 protected: 673 674 override bool runDialog(HWND owner) { 675 if(!_runDialog(owner)) { 676 if(!CommDlgExtendedError()) { 677 return false; 678 } 679 _cantrun(); 680 } 681 return true; 682 } 683 684 685 private BOOL _runDialog(HWND owner) { 686 BOOL result = 0; 687 688 beginOfn(owner); 689 690 //synchronized(typeid(dfl.internal.utf.CurDirLockType)) 691 { 692 if(dfl.internal.utf.useUnicode) { 693 enum NAME = "GetOpenFileNameW"; 694 static GetOpenFileNameWProc proc = null; 695 696 if(!proc) { 697 proc = cast(GetOpenFileNameWProc)GetProcAddress(GetModuleHandleA("comdlg32.dll"), NAME.ptr); 698 if(!proc) { 699 throw new Exception("Unable to load procedure " ~ NAME ~ ""); 700 } 701 } 702 703 result = proc(&ofnw); 704 } else { 705 result = GetOpenFileNameA(&ofna); 706 } 707 } 708 709 if(result) { 710 finishOfn(); 711 return result; 712 } 713 714 cancelOfn(); 715 return result; 716 } 717 } 718 719 720 721 class SaveFileDialog: FileDialog { // docmain 722 this() { 723 super(); 724 ofn.Flags |= OFN_OVERWRITEPROMPT; 725 } 726 727 728 override void reset() { 729 super.reset(); 730 ofn.Flags |= OFN_OVERWRITEPROMPT; 731 } 732 733 734 735 final @property void createPrompt(bool byes) { // setter 736 if(byes) { 737 ofn.Flags |= OFN_CREATEPROMPT; 738 } else { 739 ofn.Flags &= ~OFN_CREATEPROMPT; 740 } 741 } 742 743 /// ditto 744 final @property bool createPrompt() { // getter 745 return (ofn.Flags & OFN_CREATEPROMPT) != 0; 746 } 747 748 749 750 final @property void overwritePrompt(bool byes) { // setter 751 if(byes) { 752 ofn.Flags |= OFN_OVERWRITEPROMPT; 753 } else { 754 ofn.Flags &= ~OFN_OVERWRITEPROMPT; 755 } 756 } 757 758 /// ditto 759 final @property bool overwritePrompt() { // getter 760 return (ofn.Flags & OFN_OVERWRITEPROMPT) != 0; 761 } 762 763 764 private import std.stream; // TO-DO: remove this import; use dfl.internal.dlib. 765 766 767 // Opens and creates with read and write access. 768 // Warning: if file exists, it's truncated. 769 final Stream openFile() { 770 return new File(fileName(), FileMode.OutNew | FileMode.Out | FileMode.In); 771 } 772 773 774 protected: 775 776 override bool runDialog(HWND owner) { 777 beginOfn(owner); 778 779 //synchronized(typeid(dfl.internal.utf.CurDirLockType)) 780 { 781 if(dfl.internal.utf.useUnicode) { 782 enum NAME = "GetSaveFileNameW"; 783 static GetSaveFileNameWProc proc = null; 784 785 if(!proc) { 786 proc = cast(GetSaveFileNameWProc)GetProcAddress(GetModuleHandleA("comdlg32.dll"), NAME.ptr); 787 if(!proc) { 788 throw new Exception("Unable to load procedure " ~ NAME ~ ""); 789 } 790 } 791 792 if(proc(&ofnw)) { 793 finishOfn(); 794 return true; 795 } 796 } else { 797 if(GetSaveFileNameA(&ofna)) { 798 finishOfn(); 799 return true; 800 } 801 } 802 } 803 804 cancelOfn(); 805 return false; 806 } 807 } 808 809 810 private extern(Windows) LRESULT ofnHookProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) nothrow { 811 alias dfl.internal.winapi.HANDLE HANDLE; // Otherwise, odd conflict with wine. 812 813 enum PROP_STR = "DFL_FileDialog"; 814 FileDialog fd; 815 LRESULT result = 0; 816 817 try 818 { 819 if(msg == WM_INITDIALOG) { 820 OPENFILENAMEA* ofn; 821 ofn = cast(OPENFILENAMEA*)lparam; 822 SetPropA(hwnd, PROP_STR.ptr, cast(HANDLE)ofn.lCustData); 823 fd = cast(FileDialog)cast(void*)ofn.lCustData; 824 } else 825 { 826 fd = cast(FileDialog)cast(void*)GetPropA(hwnd, PROP_STR.ptr); 827 } 828 829 //cprintf("hook msg(%d/0x%X) to obj %p\n", msg, msg, fd); 830 if(fd) { 831 fd.needRebuildFiles = true; 832 result = fd.hookProc(hwnd, msg, wparam, lparam); 833 } 834 } catch(DThrowable e) { 835 Application.onThreadException(e); 836 } 837 838 return result; 839 } 840