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 }