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