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