1 // Written in the D programming language.
2 
3 /**
4 This module contains FileDialog implementation.
5 
6 Can show dialog for open / save.
7 
8 
9 Synopsis:
10 
11 ----
12 import dlangui.dialogs.filedlg;
13 
14 UIString caption = "Open File"d;
15 auto dlg = new FileDialog(caption, window, FileDialogFlag.Open);
16 dlg.show();
17 
18 ----
19 
20 Copyright: Vadim Lopatin, 2014
21 License:   Boost License 1.0
22 Authors:   Vadim Lopatin, coolreader.org@gmail.com
23 */
24 module dlangui.dialogs.filedlg;
25 
26 import dlangui.core.events;
27 import dlangui.core.i18n;
28 import dlangui.core.stdaction;
29 import dlangui.core.files;
30 import dlangui.widgets.controls;
31 import dlangui.widgets.lists;
32 import dlangui.widgets.popup;
33 import dlangui.widgets.layouts;
34 import dlangui.widgets.grid;
35 import dlangui.widgets.editors;
36 import dlangui.widgets.menu;
37 import dlangui.widgets.combobox;
38 import dlangui.platforms.common.platform;
39 import dlangui.dialogs.dialog;
40 
41 private import std.algorithm;
42 private import std.file;
43 private import std.path;
44 private import std.utf : toUTF32;
45 private import std.string;
46 private import std.array;
47 private import std.conv : to;
48 
49 
50 /// flags for file dialog options
51 enum FileDialogFlag : uint {
52     /// file must exist (use this for open dialog)
53     FileMustExist = 0x100,
54     /// ask before saving to existing
55     ConfirmOverwrite = 0x200,
56     /// select directory, not file
57     SelectDirectory = 0x400,
58     /// show Create Directory button
59     EnableCreateDirectory = 0x800,
60     /// flags for Open dialog
61     Open = FileMustExist | EnableCreateDirectory,
62     /// flags for Save dialog
63     Save = ConfirmOverwrite | EnableCreateDirectory,
64 
65 }
66 
67 /// File dialog action codes
68 enum FileDialogActions : int {
69     ShowInFileManager = 4000,
70     CreateDirectory = 4001,
71     DeleteFile = 4002,
72 }
73 
74 /// filetype filter entry for FileDialog
75 struct FileFilterEntry {
76     UIString label;
77     string[] filter;
78     bool executableOnly;
79     this(UIString displayLabel, string filterList, bool executableOnly = false) {
80         label = displayLabel;
81         if (filterList.length)
82             filter = split(filterList, ";");
83         this.executableOnly = executableOnly;
84     }
85 }
86 
87 /// sorting orders for file dialog items
88 enum FileListSortOrder {
89     NAME,
90     NAME_DESC,
91     SIZE_DESC,
92     SIZE,
93     TIMESTAMP_DESC,
94     TIMESTAMP,
95 }
96 
97 /// File open / save dialog
98 class FileDialog : Dialog, CustomGridCellAdapter {
99     protected FilePathPanel _edPath;
100     protected EditLine _edFilename;
101     protected ComboBox _cbFilters;
102     protected StringGridWidget _fileList;
103     protected FileListSortOrder _sortOrder = FileListSortOrder.NAME;
104     protected Widget leftPanel;
105     protected VerticalLayout rightPanel;
106     protected Action _action;
107 
108     protected RootEntry[] _roots;
109     protected FileFilterEntry[] _filters;
110     protected int _filterIndex;
111     protected string _path;
112     protected string _filename;
113     protected DirEntry[] _entries;
114     protected bool _isRoot;
115 
116     protected bool _isOpenDialog;
117 
118     protected bool _showHiddenFiles;
119     protected bool _allowMultipleFiles;
120 
121     protected string[string] _filetypeIcons;
122 
123     this(UIString caption, Window parent, Action action = null, uint fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist) {
124         super(caption, parent, fileDialogFlags | (Platform.instance.uiDialogDisplayMode & DialogDisplayMode.fileDialogInPopup ? DialogFlag.Popup : 0));
125         _isOpenDialog = !(_flags & FileDialogFlag.ConfirmOverwrite);
126         if (action is null) {
127             if (fileDialogFlags & FileDialogFlag.SelectDirectory)
128                 action = ACTION_OPEN_DIRECTORY.clone();
129             else if (_isOpenDialog)
130                 action = ACTION_OPEN.clone();
131             else
132                 action = ACTION_SAVE.clone();
133         }
134         _action = action;
135     }
136 
137     /// mapping of file extension to icon resource name, e.g. ".txt": "text-plain"
138     @property ref string[string] filetypeIcons() { return _filetypeIcons; }
139 
140     /// filter list for file type filter combo box
141     @property FileFilterEntry[] filters() {
142         return _filters;
143     }
144 
145     /// filter list for file type filter combo box
146     @property void filters(FileFilterEntry[] values) {
147         _filters = values;
148     }
149 
150     /// add new filter entry
151     void addFilter(FileFilterEntry value) {
152         _filters ~= value;
153     }
154 
155     /// filter index
156     @property int filterIndex() {
157         return _filterIndex;
158     }
159 
160     /// filter index
161     @property void filterIndex(int index) {
162         _filterIndex = index;
163     }
164 
165     /// the path to the directory whose files should be displayed
166     @property string path() {
167         return _path;
168     }
169 
170     @property void path(string s) {
171         _path = s;
172     }
173 
174     /// the name of the file or directory that is currently selected
175     @property string filename() {
176         return _filename;
177     }
178 
179     @property void filename(string s) {
180         _filename = s;
181     }
182 
183     /// all the selected filenames
184     @property string[] filenames() {
185         string[] res;
186         res.reserve(_fileList.selection.length);
187         int i = 0;
188         foreach (val; _fileList.selection) {
189             res ~= _entries[val.y];
190             ++i;
191         }
192         return res;
193     }
194 
195     @property bool showHiddenFiles() {
196         return _showHiddenFiles;
197     }
198 
199     @property void showHiddenFiles(bool b) {
200         _showHiddenFiles = b;
201     }
202 
203     @property bool allowMultipleFiles() {
204         return _allowMultipleFiles;
205     }
206 
207     @property void allowMultipleFiles(bool b) {
208         _allowMultipleFiles = b;
209     }
210 
211     /// return currently selected filter value - array of patterns like ["*.txt", "*.rtf"]
212     @property string[] selectedFilter() {
213         if (_filterIndex >= 0 && _filterIndex < _filters.length)
214             return _filters[_filterIndex].filter;
215         return null;
216     }
217 
218     @property bool executableFilterSelected() {
219         if (_filterIndex >= 0 && _filterIndex < _filters.length)
220             return _filters[_filterIndex].executableOnly;
221         return false;
222     }
223 
224     protected bool upLevel() {
225         return openDirectory(parentDir(_path), _path);
226     }
227 
228     protected bool reopenDirectory() {
229         return openDirectory(_path, null);
230     }
231 
232     protected void locateFileInList(dstring pattern) {
233         if (!pattern.length)
234             return;
235         int selection = _fileList.row;
236         if (selection < 0)
237             selection = 0;
238         int index = -1; // first matched item
239         string mask = pattern.toUTF8;
240         // search forward from current row to end of list
241         for(int i = selection; i < _entries.length; i++) {
242             string fname = baseName(_entries[i].name);
243             if (fname.startsWith(mask)) {
244                 index = i;
245                 break;
246             }
247         }
248         if (index < 0) {
249             // search from beginning of list to current position
250             for(int i = 0; i < selection && i < _entries.length; i++) {
251                 string fname = baseName(_entries[i].name);
252                 if (fname.startsWith(mask)) {
253                     index = i;
254                     break;
255                 }
256             }
257         }
258         if (index >= 0) {
259             // move selection
260             _fileList.selectCell(1, index + 1);
261             window.update();
262         }
263     }
264 
265     /// change sort order after clicking on column col
266     protected void changeSortOrder(int col) {
267         assert(col >= 2 && col <= 4);
268         // 2=NAME, 3=SIZE, 4=MODIFIED
269         col -= 2;
270         int n = col * 2;
271         if ((n & 0xFE) == ((cast(int)_sortOrder) & 0xFE)) {
272             // invert DESC / ASC if clicked same column as in current sorting order
273             _sortOrder = cast(FileListSortOrder)(_sortOrder ^ 1);
274         } else {
275             _sortOrder = cast(FileListSortOrder)n;
276         }
277         string selectedItemPath;
278         int currentRow = _fileList.row;
279         if (currentRow >= 0 && currentRow < _entries.length) {
280             selectedItemPath = _entries[currentRow].name;
281         }
282         updateColumnHeaders();
283         sortEntries();
284         entriesToCells(selectedItemPath);
285         requestLayout();
286         if (window)
287             window.update();
288     }
289 
290     /// predicate for sorting items - NAME
291     static bool compareItemsByName(ref DirEntry item1, ref DirEntry item2) {
292         return ((item1.isDir && !item2.isDir) || ((item1.isDir == item2.isDir) && (item1.name < item2.name)));
293     }
294     /// predicate for sorting items - NAME DESC
295     static bool compareItemsByNameDesc(ref DirEntry item1, ref DirEntry item2) {
296         return ((item1.isDir && !item2.isDir) || ((item1.isDir == item2.isDir) && (item1.name > item2.name)));
297     }
298     /// predicate for sorting items - SIZE
299     static bool compareItemsBySize(ref DirEntry item1, ref DirEntry item2) {
300         return ((item1.isDir && !item2.isDir)
301                 || ((item1.isDir && item2.isDir) && (item1.name < item2.name))
302                 || ((!item1.isDir && !item2.isDir) && (item1.size < item2.size))
303                 );
304     }
305     /// predicate for sorting items - SIZE DESC
306     static bool compareItemsBySizeDesc(ref DirEntry item1, ref DirEntry item2) {
307         return ((item1.isDir && !item2.isDir)
308                 || ((item1.isDir && item2.isDir) && (item1.name < item2.name))
309                 || ((!item1.isDir && !item2.isDir) && (item1.size > item2.size))
310                 );
311     }
312     /// predicate for sorting items - TIMESTAMP
313     static bool compareItemsByTimestamp(ref DirEntry item1, ref DirEntry item2) {
314         try {
315             return item1.timeLastModified < item2.timeLastModified;
316         } catch (Exception e) {
317             return false;
318         }
319     }
320     /// predicate for sorting items - TIMESTAMP DESC
321     static bool compareItemsByTimestampDesc(ref DirEntry item1, ref DirEntry item2) {
322         try {
323             return item1.timeLastModified > item2.timeLastModified;
324         } catch (Exception e) {
325             return false;
326         }
327     }
328 
329     /// sort entries according to _sortOrder
330     protected void sortEntries() {
331         if (_entries.length < 1)
332             return;
333         DirEntry[] entriesToSort = _entries[0..$];
334         if (_entries.length > 0) {
335             string fname = baseName(_entries[0].name);
336             if (fname == "..") {
337                 entriesToSort = _entries[1..$];
338             }
339         }
340         import std.algorithm.sorting : sort;
341         switch(_sortOrder) with(FileListSortOrder) {
342             default:
343             case NAME:
344                 sort!compareItemsByName(entriesToSort);
345                 break;
346             case NAME_DESC:
347                 sort!compareItemsByNameDesc(entriesToSort);
348                 break;
349             case SIZE:
350                 sort!compareItemsBySize(entriesToSort);
351                 break;
352             case SIZE_DESC:
353                 sort!compareItemsBySizeDesc(entriesToSort);
354                 break;
355             case TIMESTAMP:
356                 sort!compareItemsByTimestamp(entriesToSort);
357                 break;
358             case TIMESTAMP_DESC:
359                 sort!compareItemsByTimestampDesc(entriesToSort);
360                 break;
361         }
362     }
363 
364     protected string formatTimestamp(ref DirEntry f) {
365         import std.datetime : SysTime;
366         import std.typecons : Nullable;
367         Nullable!SysTime ts;
368         try {
369             ts = f.timeLastModified;
370         } catch (Exception e) {
371             Log.w(e.msg);
372         }
373         if (ts.isNull) {
374             return "----.--.-- --:--";
375         } else {
376             return "%04d.%02d.%02d %02d:%02d".format(ts.get.year, ts.get.month, ts.get.day, ts.get.hour, ts.get.minute);
377         }
378     }
379 
380     protected int entriesToCells(string selectedItemPath) {
381         _fileList.rows = cast(int)_entries.length;
382         int selectionIndex = -1;
383         for (int i = 0; i < _entries.length; i++) {
384             if (_entries[i].name.equal(selectedItemPath))
385                 selectionIndex = i;
386             string fname = baseName(_entries[i].name);
387             string sz;
388             string date;
389             bool d = _entries[i].isDir;
390             _fileList.setCellText(1, i, UIString.fromRaw(toUTF32(fname)));
391             if (d) {
392                 _fileList.setCellText(0, i, UIString.fromRaw("folder"));
393                 if (fname != "..")
394                     date = formatTimestamp(_entries[i]);
395             } else {
396                 string ext = extension(fname);
397                 string resname;
398                 if (ext in _filetypeIcons)
399                     resname = _filetypeIcons[ext];
400                 else if (baseName(fname) in _filetypeIcons)
401                     resname = _filetypeIcons[baseName(fname)];
402                 else
403                     resname = "text-plain";
404                 _fileList.setCellText(0, i, UIString.fromRaw(toUTF32(resname)));
405                 double size = double.nan;
406                 try {
407                     size = _entries[i].size;
408                 } catch (Exception e) {
409                     Log.w(e.msg);
410                 }
411                 import std.math : isNaN;
412                 if (size.isNaN)
413                     sz = "--";
414                 else {
415                     import std.format : format;
416                     sz = size < 1024 ? to!string(size) ~ " B" :
417                     (size < 1024*1024 ? "%.1f".format(size/1024) ~ " KB" :
418                      (size < 1024*1024*1024 ? "%.1f".format(size/(1024*1024)) ~ " MB" :
419                       "%.1f".format(size/(1024*1024*1024)) ~ " GB"));
420                 }
421                 date = formatTimestamp(_entries[i]);
422             }
423             _fileList.setCellText(2, i, UIString.fromRaw(toUTF32(sz)));
424             _fileList.setCellText(3, i, UIString.fromRaw(toUTF32(date)));
425         }
426         if(_fileList.height > 0)
427             _fileList.scrollTo(0, 0);
428 
429         autofitGrid();
430         if (selectionIndex >= 0)
431             _fileList.selectCell(1, selectionIndex + 1, true);
432         else if (_entries.length > 0)
433             _fileList.selectCell(1, 1, true);
434         return selectionIndex;
435     }
436 
437     protected bool openDirectory(string dir, string selectedItemPath) {
438         dir = buildNormalizedPath(dir);
439         Log.d("FileDialog.openDirectory(", dir, ")");
440         DirEntry[] entries;
441 
442         auto attrFilter = (showHiddenFiles ? AttrFilter.all : AttrFilter.allVisible) | AttrFilter.special | AttrFilter.parent;
443         if (executableFilterSelected()) {
444             attrFilter |= AttrFilter.executable;
445         }
446         if (_action.id == ACTION_OPEN_DIRECTORY.id) {
447           attrFilter = AttrFilter.dirs;
448           if (showHiddenFiles)
449             attrFilter |= AttrFilter.hidden;
450         }
451         try {
452             _entries = listDirectory(dir, attrFilter, selectedFilter());
453         } catch(Exception e) {
454             Log.e("Cannot list directory " ~ dir, e);
455             //import dlangui.dialogs.msgbox;
456             //auto msgBox = new MessageBox(UIString.fromId("MESSAGE_ERROR"c), UIString.fromRaw(e.msg.toUTF32), window());
457             //msgBox.show();
458             //return false;
459             // show empty dir if failed to read
460         }
461         _fileList.rows = 0;
462         _path = dir;
463         _isRoot = isRoot(dir);
464         _edPath.path = _path; //toUTF32(_path);
465         int selectionIndex = entriesToCells(selectedItemPath);
466         return true;
467     }
468 
469     void autofitGrid() {
470         _fileList.autoFitColumnWidths();
471         //_fileList.setColWidth(1, 0);
472         _fileList.fillColumnWidth(1);
473     }
474 
475     override bool onKeyEvent(KeyEvent event) {
476         if (event.action == KeyAction.KeyDown) {
477             if (event.keyCode == KeyCode.BACK && event.flags == 0) {
478                 upLevel();
479                 return true;
480             }
481         }
482         return super.onKeyEvent(event);
483     }
484 
485     /// return true for custom drawn cell
486     override bool isCustomCell(int col, int row) {
487         if ((col == 0 || col == 1) && row >= 0)
488             return true;
489         return false;
490     }
491 
492     protected DrawableRef rowIcon(int row) {
493         string iconId = toUTF8(_fileList.cellText(0, row));
494         DrawableRef res;
495         if (iconId.length)
496             res = drawableCache.get(iconId);
497         return res;
498     }
499 
500     /// return cell size
501     override Point measureCell(int col, int row) {
502         if (col == 1) {
503             FontRef fnt = _fileList.font;
504             dstring txt = _fileList.cellText(col, row);
505             Point sz = fnt.textSize(txt);
506             if (sz.y < fnt.height)
507                 sz.y = fnt.height;
508             return sz;
509         }
510         if (WIDGET_STYLE_CONSOLE)
511             return Point(0, 0);
512         else {
513             DrawableRef icon = rowIcon(row);
514             if (icon.isNull)
515                 return Point(0, 0);
516             return Point(icon.width + 2.pointsToPixels, icon.height + 2.pointsToPixels);
517         }
518     }
519 
520     /// draw data cell content
521     override void drawCell(DrawBuf buf, Rect rc, int col, int row) {
522         if (col == 1) {
523             if (BACKEND_GUI)
524                 rc.shrink(2, 1);
525             else
526                 rc.right--;
527             FontRef fnt = _fileList.font;
528             dstring txt = _fileList.cellText(col, row);
529             Point sz = fnt.textSize(txt);
530             Align ha = Align.Left;
531             //if (sz.y < rc.height)
532             //    applyAlign(rc, sz, ha, Align.VCenter);
533             int offset = WIDGET_STYLE_CONSOLE ? 0 : 1;
534             uint cl = _fileList.textColor;
535             if (_entries[row].isDir)
536                 cl = style.customColor("file_dialog_dir_name_color", cl);
537             fnt.drawText(buf, rc.left + offset, rc.top + offset, txt, cl);
538             return;
539         }
540         DrawableRef img = rowIcon(row);
541         if (!img.isNull) {
542             Point sz;
543             sz.x = img.width;
544             sz.y = img.height;
545             applyAlign(rc, sz, Align.HCenter, Align.VCenter);
546             uint st = state;
547             img.drawTo(buf, rc, st);
548         }
549     }
550 
551     protected ListWidget createRootsList() {
552         ListWidget res = new ListWidget("ROOTS_LIST");
553         res.styleId = STYLE_LIST_BOX;
554         WidgetListAdapter adapter = new WidgetListAdapter();
555         foreach(ref RootEntry root; _roots) {
556             ImageTextButton btn = new ImageTextButton(null, root.icon, root.label);
557             static if (WIDGET_STYLE_CONSOLE) btn.margins = Rect(1, 1, 0, 0);
558             btn.orientation = Orientation.Vertical;
559             btn.styleId = STYLE_TRANSPARENT_BUTTON_BACKGROUND;
560             btn.focusable = false;
561             btn.tooltipText = root.path.toUTF32;
562             adapter.add(btn);
563         }
564         res.ownAdapter = adapter;
565         res.layoutWidth(WRAP_CONTENT).layoutHeight(FILL_PARENT).layoutWeight(0);
566         res.itemClick = delegate(Widget source, int itemIndex) {
567             openDirectory(_roots[itemIndex].path, null);
568             res.selectItem(-1);
569             return true;
570         };
571         res.focusable = true;
572         debug Log.d("root list styleId=", res.styleId);
573         return res;
574     }
575 
576     /// file list item activated (double clicked or Enter key pressed)
577     protected void onItemActivated(int index) {
578         DirEntry e = _entries[index];
579         if (e.isDir) {
580             openDirectory(e.name, _path);
581         } else if (e.isFile) {
582             string fname = e.name;
583             if ((_flags & FileDialogFlag.ConfirmOverwrite) && exists(fname) && isFile(fname)) {
584                 showConfirmOverwriteQuestion(fname);
585                 return;
586             }
587             else {
588                 Action result = _action;
589                 result.stringParam = fname;
590                 close(result);
591             }
592         }
593     }
594 
595     /// file list item selected
596     protected void onItemSelected(int index) {
597         DirEntry e = _entries[index];
598         string fname = e.name;
599         _edFilename.text = toUTF32(baseName(fname));
600         _filename = fname;
601     }
602 
603     protected void createAndEnterDirectory(string name) {
604         string newdir = buildNormalizedPath(_path, name);
605         try {
606             mkdirRecurse(newdir);
607             openDirectory(newdir, null);
608         } catch (Exception e) {
609             window.showMessageBox(UIString.fromId("CREATE_FOLDER_ERROR_TITLE"c), UIString.fromId("CREATE_FOLDER_ERROR_MESSAGE"c));
610         }
611     }
612 
613     /// calls close with default action; returns true if default action is found and invoked
614     override protected bool closeWithDefaultAction() {
615         return handleAction(_action);
616     }
617 
618     /// Custom handling of actions
619     override bool handleAction(const Action action) {
620         if (action.id == StandardAction.Cancel) {
621             super.handleAction(action);
622             return true;
623         }
624         if (action.id == FileDialogActions.ShowInFileManager) {
625             Platform.instance.showInFileManager(action.stringParam);
626             return true;
627         }
628         if (action.id == StandardAction.CreateDirectory) {
629             // show editor popup
630             window.showInputBox(UIString.fromId("CREATE_NEW_FOLDER"c), UIString.fromId("INPUT_NAME_FOR_FOLDER"c), ""d, delegate(dstring s) {
631                 if (!s.empty)
632                     createAndEnterDirectory(toUTF8(s));
633             });
634             return true;
635         }
636         if (action.id == StandardAction.Open || action.id == StandardAction.OpenDirectory || action.id == StandardAction.Save) {
637             auto baseFilename = toUTF8(_edFilename.text);
638             if (action.id == StandardAction.OpenDirectory)
639                 _filename = _path ~ dirSeparator;
640             else
641                 _filename = _path ~ dirSeparator ~ baseFilename;
642 
643             if (action.id != StandardAction.OpenDirectory && exists(_filename) && isDir(_filename)) {
644                 // directory name in _edFileName.text but we need file so open directory
645                 openDirectory(_filename, null);
646                 return true;
647             } else if (baseFilename.length > 0) {
648                 Action result = _action;
649                 result.stringParam = _filename;
650                 // success if either selected dir & has to open dir or if selected file
651                 if (action.id == StandardAction.OpenDirectory && exists(_filename) && isDir(_filename)) {
652                     close(result);
653                     return true;
654                 }
655                 else if (action.id == StandardAction.Save && !(_flags & FileDialogFlag.FileMustExist)) {
656                     // save dialog
657                     if ((_flags & FileDialogFlag.ConfirmOverwrite) && exists(_filename) && isFile(_filename)) {
658                         showConfirmOverwriteQuestion(_filename);
659                         return true;
660                     }
661                     else {
662                         close(result);
663                         return true;
664                     }
665                 }
666                 else if (!(_flags & FileDialogFlag.FileMustExist) || (exists(_filename) && isFile(_filename))) {
667                     // open dialog
668                     close(result);
669                     return true;
670                 }
671             }
672         }
673         return super.handleAction(action);
674     }
675 
676     /// shows question "override file?"
677     protected void showConfirmOverwriteQuestion(string fileName) {
678         window.showMessageBox(UIString.fromId("CONFIRM_OVERWRITE_TITLE"c).value, format(UIString.fromId("CONFIRM_OVERWRITE_FILE_NAMED_%s_QUESTION"c).value, baseName(fileName)), [ACTION_YES, ACTION_NO], 1, delegate bool(const Action a) {
679             if (a.id == StandardAction.Yes) {
680                 Action result = _action;
681                 result.stringParam = fileName;
682                 close(result);
683             }
684             return true;
685         });
686     }
687 
688     bool onPathSelected(string path) {
689         //
690         return openDirectory(path, null);
691     }
692 
693     protected MenuItem getCellPopupMenu(GridWidgetBase source, int col, int row) {
694         if (row >= 0 && row < _entries.length) {
695             MenuItem item = new MenuItem();
696             DirEntry e = _entries[row];
697             // show in explorer action
698             auto showAction = new Action(FileDialogActions.ShowInFileManager, "ACTION_FILE_SHOW_IN_FILE_MANAGER"c);
699             showAction.stringParam = e.name;
700             item.add(showAction);
701             // create directory action
702             if (_flags & FileDialogFlag.EnableCreateDirectory)
703                 item.add(ACTION_CREATE_DIRECTORY);
704 
705             if (e.isDir) {
706                 //_edFilename.text = ""d;
707                 //_filename = "";
708             } else if (e.isFile) {
709                 //string fname = e.name;
710                 //_edFilename.text = toUTF32(baseName(fname));
711                 //_filename = fname;
712             }
713             return item;
714         }
715         return null;
716     }
717 
718     /// override to implement creation of dialog controls
719     override void initialize() {
720         // remember filename specified by user, file grid initialization can change it
721         string defaultFilename = _filename;
722 
723         _roots = getRootPaths() ~ getBookmarkPaths();
724 
725         layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).minWidth(WIDGET_STYLE_CONSOLE ? 50 : 600);
726         //minHeight = 400;
727 
728         LinearLayout content = new HorizontalLayout("dlgcontent");
729 
730         content.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); //.minWidth(400).minHeight(300);
731 
732 
733         //leftPanel = new VerticalLayout("places");
734         //leftPanel.addChild(createRootsList());
735         //leftPanel.layoutHeight(FILL_PARENT).minWidth(WIDGET_STYLE_CONSOLE ? 7 : 40);
736 
737         leftPanel = createRootsList();
738         leftPanel.minWidth(WIDGET_STYLE_CONSOLE ? 7 : 40.pointsToPixels);
739 
740         rightPanel = new VerticalLayout("main");
741         rightPanel.layoutHeight(FILL_PARENT).layoutWidth(FILL_PARENT);
742         rightPanel.addChild(new TextWidget(null, UIString.fromId("MESSAGE_PATH"c) ~ ":"));
743 
744         content.addChild(leftPanel);
745         content.addChild(rightPanel);
746 
747         _edPath = new FilePathPanel("path");
748         _edPath.layoutWidth(FILL_PARENT);
749         _edPath.layoutWeight = 0;
750         _edPath.onPathSelectionListener = &onPathSelected;
751         HorizontalLayout fnlayout = new HorizontalLayout();
752         fnlayout.layoutWidth(FILL_PARENT);
753         _edFilename = new EditLine("filename");
754         _edFilename.layoutWidth(FILL_PARENT);
755         _edFilename.setDefaultPopupMenu();
756         if (_flags & FileDialogFlag.SelectDirectory) {
757             _edFilename.visibility = Visibility.Gone;
758         }
759 
760         //_edFilename.layoutWeight = 0;
761         fnlayout.addChild(_edFilename);
762         if (_filters.length) {
763             dstring[] filterLabels;
764             foreach(f; _filters)
765                 filterLabels ~= f.label.value;
766             _cbFilters = new ComboBox("filter", filterLabels);
767             _cbFilters.selectedItemIndex = _filterIndex;
768             _cbFilters.itemClick = delegate(Widget source, int itemIndex) {
769                 _filterIndex = itemIndex;
770                 reopenDirectory();
771                 return true;
772             };
773             _cbFilters.layoutWidth(WRAP_CONTENT);
774             _cbFilters.layoutWeight(0);
775             //_cbFilters.backgroundColor = 0xFFC0FF;
776             fnlayout.addChild(_cbFilters);
777             //fnlayout.backgroundColor = 0xFFFFC0;
778         }
779 
780         _fileList = new StringGridWidget("files");
781         _fileList.styleId = STYLE_FILE_DIALOG_GRID;
782         _fileList.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
783         _fileList.fullColumnOnLeft(false);
784         _fileList.fullRowOnTop(false);
785         _fileList.resize(4, 3);
786         _fileList.setColTitle(0, UIString.fromRaw(" "));
787         updateColumnHeaders();
788         _fileList.showRowHeaders = false;
789         _fileList.rowSelect = true;
790         _fileList.multiSelect = _allowMultipleFiles;
791         _fileList.cellPopupMenu = &getCellPopupMenu;
792         _fileList.menuItemAction = &handleAction;
793         _fileList.minVisibleRows = 10;
794         _fileList.minVisibleCols = 4;
795         _fileList.headerCellClicked = &onHeaderCellClicked;
796 
797         _fileList.keyEvent = delegate(Widget source, KeyEvent event) {
798             if (_shortcutHelper.onKeyEvent(event))
799                 locateFileInList(_shortcutHelper.text);
800             return false;
801         };
802 
803         rightPanel.addChild(_edPath);
804         rightPanel.addChild(_fileList);
805         rightPanel.addChild(fnlayout);
806 
807 
808         addChild(content);
809         if (_flags & FileDialogFlag.EnableCreateDirectory) {
810             addChild(createButtonsPanel([ACTION_CREATE_DIRECTORY, cast(immutable)_action, ACTION_CANCEL], 1, 1));
811         } else {
812             addChild(createButtonsPanel([cast(immutable)_action, ACTION_CANCEL], 0, 0));
813         }
814 
815         _fileList.customCellAdapter = this;
816         _fileList.cellActivated = delegate(GridWidgetBase source, int col, int row) {
817             onItemActivated(row);
818         };
819         _fileList.cellSelected = delegate(GridWidgetBase source, int col, int row) {
820             onItemSelected(row);
821         };
822 
823         if (_path.empty || !_path.exists || !_path.isDir) {
824             _path = currentDir;
825             if (!_path.exists || !_path.isDir)
826                 _path = homePath;
827         }
828         openDirectory(_path, _filename);
829         _fileList.layoutHeight = FILL_PARENT;
830 
831         // set default file name if specified by user
832         if (defaultFilename.length != 0)
833             _edFilename.text = toUTF32(baseName(defaultFilename));
834     }
835 
836     /// get sort order suffix for column title
837     protected UIString appendSortOrderSuffix(UIString columnName, FileListSortOrder arrowUp, FileListSortOrder arrowDown)
838     {
839         if (_sortOrder == arrowUp)
840             return UIString.fromRaw(columnName ~ UIString.fromRaw(" ▲"d));
841         if (_sortOrder == arrowDown)
842             return UIString.fromRaw(columnName ~ UIString.fromRaw(" ▼"d));
843         return columnName;
844     }
845 
846     protected void updateColumnHeaders() {
847         _fileList.setColTitle(1, appendSortOrderSuffix(UIString.fromId("COL_NAME"c), FileListSortOrder.NAME_DESC, FileListSortOrder.NAME));
848         _fileList.setColTitle(2, appendSortOrderSuffix(UIString.fromId("COL_SIZE"c), FileListSortOrder.SIZE_DESC, FileListSortOrder.SIZE));
849         _fileList.setColTitle(3, appendSortOrderSuffix(UIString.fromId("COL_MODIFIED"c), FileListSortOrder.TIMESTAMP_DESC, FileListSortOrder.TIMESTAMP));
850     }
851 
852     protected void onHeaderCellClicked(GridWidgetBase source, int col, int row) {
853         debug Log.d("onHeaderCellClicked col=", col, " row=", row);
854         if (row == 0 && col >= 2 && col <= 4) {
855             // 2=NAME, 3=SIZE, 4=MODIFIED
856             changeSortOrder(col);
857         }
858     }
859 
860     protected TextTypingShortcutHelper _shortcutHelper;
861 
862     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
863     override void layout(Rect rc) {
864         super.layout(rc);
865         autofitGrid();
866     }
867 
868 
869     ///// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
870     //override void measure(int parentWidth, int parentHeight) {
871     //    super.measure(parentWidth, parentHeight);
872     //    for(int i = 0; i < childCount; i++) {
873     //        Widget w = child(i);
874     //        Log.d("id=", w.id, " measuredHeight=", w.measuredHeight );
875     //        for (int j = 0; j < w.childCount; j++) {
876     //            Widget w2 = w.child(j);
877     //            Log.d("    id=", w2.id, " measuredHeight=", w.measuredHeight );
878     //        }
879     //    }
880     //    Log.d("this id=", id, " measuredHeight=", measuredHeight);
881     //}
882 
883     override void onShow() {
884         _fileList.setFocus();
885     }
886 }
887 
888 interface OnPathSelectionHandler {
889     bool onPathSelected(string path);
890 }
891 
892 class FilePathPanelItem : HorizontalLayout {
893     protected string _path;
894     protected TextWidget _text;
895     protected ImageButton _button;
896     Listener!OnPathSelectionHandler onPathSelectionListener;
897     this(string path) {
898         super(null);
899         styleId = STYLE_LIST_ITEM;
900         _path = path;
901         string fname = isRoot(path) ? path : baseName(path);
902         _text = new TextWidget(null, toUTF32(fname));
903         _text.styleId = STYLE_BUTTON_TRANSPARENT;
904         _text.clickable = true;
905         _text.click = &onTextClick;
906         //_text.backgroundColor = 0xC0FFFF;
907         _text.state = State.Parent;
908         _button = new ImageButton(null, ATTR_SCROLLBAR_BUTTON_RIGHT);
909         _button.styleId = STYLE_BUTTON_TRANSPARENT;
910         _button.focusable = false;
911         _button.click = &onButtonClick;
912         //_button.backgroundColor = 0xC0FFC0;
913         _button.state = State.Parent;
914         trackHover(true);
915         addChild(_text);
916         addChild(_button);
917         margins(Rect(2.pointsToPixels + 1, 0, 2.pointsToPixels + 1, 0));
918     }
919 
920     private bool onTextClick(Widget src) {
921         if (onPathSelectionListener.assigned)
922             return onPathSelectionListener(_path);
923         return false;
924     }
925     private bool onButtonClick(Widget src) {
926         // show popup menu with subdirs
927         string[] filters;
928         DirEntry[] entries;
929         try {
930             AttrFilter attrFilter = AttrFilter.dirs | AttrFilter.parent;
931             entries = listDirectory(_path, attrFilter);
932         } catch(Exception e) {
933             return false;
934         }
935         if (entries.length == 0)
936             return false;
937         MenuItem dirs = new MenuItem();
938         int itemId = 25000;
939         foreach(ref DirEntry e; entries) {
940             string fullPath = e.name;
941             string d = baseName(fullPath);
942             Action a = new Action(itemId++, toUTF32(d));
943             a.stringParam = fullPath;
944             MenuItem item = new MenuItem(a);
945             item.menuItemAction = delegate(const Action action) {
946                 if (onPathSelectionListener.assigned)
947                     return onPathSelectionListener(action.stringParam);
948                 return false;
949             };
950             dirs.add(item);
951         }
952         PopupMenu menuWidget = new PopupMenu(dirs);
953         PopupWidget popup = window.showPopup(menuWidget, this, PopupAlign.Below);
954         popup.flags = PopupFlags.CloseOnClickOutside;
955         return true;
956     }
957 }
958 
959 /// Panel with buttons - path segments - for fast navigation to subdirs.
960 class FilePathPanelButtons : WidgetGroupDefaultDrawing {
961     protected string _path;
962     Listener!OnPathSelectionHandler onPathSelectionListener;
963     protected bool onPathSelected(string path) {
964         if (onPathSelectionListener.assigned) {
965             return onPathSelectionListener(path);
966         }
967         return false;
968     }
969     this(string ID = null) {
970         super(ID);
971         layoutWidth = FILL_PARENT;
972         clickable = true;
973     }
974     protected void initialize(string path) {
975         _path = path;
976         _children.clear();
977         string itemPath = path;
978         for (;;) {
979             FilePathPanelItem item = new FilePathPanelItem(itemPath);
980             item.onPathSelectionListener = &onPathSelected;
981             addChild(item);
982             if (isRoot(itemPath)) {
983                 break;
984             }
985             itemPath = parentDir(itemPath);
986         }
987     }
988     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
989     override void measure(int parentWidth, int parentHeight) {
990         Rect m = margins;
991         Rect p = padding;
992 
993         // calc size constraints for children
994         int pwidth = 0;
995         int pheight = 0;
996         if (parentWidth != SIZE_UNSPECIFIED)
997             pwidth = parentWidth - (m.left + m.right + p.left + p.right);
998 
999         if (parentHeight != SIZE_UNSPECIFIED)
1000             pheight = parentHeight - (m.top + m.bottom + p.top + p.bottom);
1001         int reservedForEmptySpace = parentWidth / 20;
1002         if (reservedForEmptySpace > 40.pointsToPixels)
1003             reservedForEmptySpace = 40.pointsToPixels;
1004         if (reservedForEmptySpace < 4.pointsToPixels)
1005             reservedForEmptySpace = 4.pointsToPixels;
1006 
1007         Point sz;
1008         sz.x += reservedForEmptySpace;
1009         // measure children
1010         bool exceeded = false;
1011         for (int i = 0; i < _children.count; i++) {
1012             Widget item = _children.get(i);
1013             item.measure(pwidth, pheight);
1014             if (sz.y < item.measuredHeight)
1015                 sz.y = item.measuredHeight;
1016             if (sz.x + item.measuredWidth > pwidth) {
1017                 exceeded = true;
1018             }
1019             if (!exceeded || i == 0) // at least one item must be measured
1020                 sz.x += item.measuredWidth;
1021         }
1022         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
1023     }
1024     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
1025     override void layout(Rect rc) {
1026         //Log.d("tabControl.layout enter");
1027         _needLayout = false;
1028         if (visibility == Visibility.Gone) {
1029             return;
1030         }
1031         _pos = rc;
1032         applyMargins(rc);
1033         applyPadding(rc);
1034 
1035         int reservedForEmptySpace = rc.width / 20;
1036         if (reservedForEmptySpace > 40)
1037             reservedForEmptySpace = 40;
1038         if (reservedForEmptySpace < 4)
1039             reservedForEmptySpace = 4;
1040         int maxw = rc.width - reservedForEmptySpace;
1041         int totalw = 0;
1042         int visibleItems = 0;
1043         bool exceeded = false;
1044         // measure and update visibility
1045         for (int i = 0; i < _children.count; i++) {
1046             Widget item = _children.get(i);
1047             item.visibility = Visibility.Visible;
1048             item.measure(rc.width, rc.height);
1049             if (totalw + item.measuredWidth > rc.width) {
1050                 exceeded = true;
1051             }
1052             if (!exceeded || i == 0) { // at least one item must be visible
1053                 totalw += item.measuredWidth;
1054                 visibleItems++;
1055             } else
1056                 item.visibility = Visibility.Gone;
1057         }
1058         // layout visible items
1059         // backward order
1060         Rect itemRect = rc;
1061         for (int i = visibleItems - 1; i >= 0; i--) {
1062             Widget item = _children.get(i);
1063             int w = item.measuredWidth;
1064             if (i == visibleItems - 1 && w > maxw)
1065                 w = maxw;
1066             itemRect.right = itemRect.left + w;
1067             item.layout(itemRect);
1068             itemRect.left += w;
1069         }
1070 
1071     }
1072 
1073 
1074 }
1075 
1076 interface PathSelectedHandler {
1077     bool onPathSelected(string path);
1078 }
1079 
1080 /// Panel - either path segment buttons or text editor line
1081 class FilePathPanel : FrameLayout {
1082     Listener!OnPathSelectionHandler onPathSelectionListener;
1083     static const ID_SEGMENTS = "SEGMENTS";
1084     static const ID_EDITOR = "ED_PATH";
1085     protected FilePathPanelButtons _segments;
1086     protected EditLine _edPath;
1087     protected string _path;
1088     Signal!PathSelectedHandler pathListener;
1089     this(string ID = null) {
1090         super(ID);
1091         _segments = new FilePathPanelButtons(ID_SEGMENTS);
1092         _edPath = new EditLine(ID_EDITOR);
1093         _edPath.layoutWidth = FILL_PARENT;
1094         _edPath.enterKey = &onEnterKey;
1095         _edPath.focusChange = &onEditorFocusChanged;
1096         _segments.click = &onSegmentsClickOutside;
1097         _segments.onPathSelectionListener = &onPathSelected;
1098         addChild(_segments);
1099         addChild(_edPath);
1100     }
1101 
1102     void setDefaultPopupMenu() {
1103         _edPath.setDefaultPopupMenu();
1104     }
1105 
1106     protected bool onEditorFocusChanged(Widget source, bool focused) {
1107         if (!focused) {
1108             _edPath.text = toUTF32(_path);
1109             showChild(ID_SEGMENTS);
1110         }
1111         return true;
1112     }
1113     protected bool onPathSelected(string path) {
1114         if (onPathSelectionListener.assigned) {
1115             if (exists(path))
1116                 return onPathSelectionListener(path);
1117         }
1118         return false;
1119     }
1120     protected bool onSegmentsClickOutside(Widget w) {
1121         // switch to editor
1122         _edPath.text = toUTF32(_path);
1123         showChild(ID_EDITOR);
1124         _edPath.setFocus();
1125         return true;
1126     }
1127     protected bool onEnterKey(EditWidgetBase editor) {
1128         string fn = buildNormalizedPath(toUTF8(_edPath.text));
1129         if (exists(fn) && isDir(fn))
1130             onPathSelected(fn);
1131         return true;
1132     }
1133 
1134     @property void path(string value) {
1135         _segments.initialize(value);
1136         _edPath.text = toUTF32(value);
1137         _path = value;
1138         showChild(ID_SEGMENTS);
1139     }
1140     @property string path() {
1141         return _path;
1142     }
1143 }
1144 
1145 class FileNameEditLine : HorizontalLayout {
1146     protected EditLine _edFileName;
1147     protected Button _btn;
1148     protected string[string] _filetypeIcons;
1149     protected dstring _caption;
1150     protected uint _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist | FileDialogFlag.EnableCreateDirectory;
1151     protected FileFilterEntry[] _filters;
1152     protected int _filterIndex;
1153 
1154     /// Modified state change listener (e.g. content has been saved, or first time modified after save)
1155     Signal!ModifiedStateListener modifiedStateChange;
1156     /// editor content is changed
1157     Signal!EditableContentChangeListener contentChange;
1158 
1159     @property ref Signal!EditorActionHandler editorAction() {
1160         return _edFileName.editorAction;
1161     }
1162 
1163     /// handle Enter key press inside line editor
1164     @property ref Signal!EnterKeyHandler enterKey() {
1165         return _edFileName.enterKey;
1166     }
1167 
1168     void setDefaultPopupMenu() {
1169         _edFileName.setDefaultPopupMenu();
1170     }
1171 
1172     this(string ID = null) {
1173         super(ID);
1174         _caption = UIString.fromId("TITLE_OPEN_FILE"c).value;
1175         _edFileName = new EditLine("FileNameEditLine_edFileName");
1176         _edFileName.minWidth(WIDGET_STYLE_CONSOLE ? 16 : 200);
1177         _edFileName.layoutWidth = FILL_PARENT;
1178         _btn = new Button("FileNameEditLine_btnFile", "..."d);
1179         _btn.styleId = STYLE_BUTTON_NOMARGINS;
1180         _btn.layoutWeight = 0;
1181         _btn.click = delegate(Widget src) {
1182             FileDialog dlg = new FileDialog(UIString.fromRaw(_caption), window, null, _fileDialogFlags);
1183             foreach(key, value; _filetypeIcons)
1184                 dlg.filetypeIcons[key] = value;
1185             dlg.filters = _filters;
1186             dlg.dialogResult = delegate(Dialog dlg, const Action result) {
1187                 if (result.id == ACTION_OPEN.id || result.id == ACTION_OPEN_DIRECTORY.id) {
1188                     _edFileName.text = toUTF32(result.stringParam);
1189                     if (contentChange.assigned)
1190                         contentChange(_edFileName.content);
1191                 }
1192             };
1193             string path = toUTF8(_edFileName.text);
1194             if (!path.empty) {
1195                 if (exists(path) && isFile(path)) {
1196                     dlg.path = dirName(path);
1197                     dlg.filename = baseName(path);
1198                 } else if (exists(path) && isDir(path)) {
1199                     dlg.path = path;
1200                 }
1201             }
1202             dlg.show();
1203             return true;
1204         };
1205         _edFileName.contentChange = delegate(EditableContent content) {
1206             if (contentChange.assigned)
1207                 contentChange(content);
1208         };
1209         _edFileName.modifiedStateChange = delegate(Widget src, bool modified) {
1210             if (modifiedStateChange.assigned)
1211                 modifiedStateChange(src, modified);
1212         };
1213         addChild(_edFileName);
1214         addChild(_btn);
1215     }
1216 
1217     @property uint fileDialogFlags() { return _fileDialogFlags; }
1218     @property void fileDialogFlags(uint f) { _fileDialogFlags = f; }
1219 
1220     @property dstring caption() { return _caption; }
1221     @property void caption(dstring s) { _caption = s; }
1222 
1223     /// returns widget content text (override to support this)
1224     override @property dstring text() const { return _edFileName.text; }
1225     /// sets widget content text (override to support this)
1226     override @property Widget text(dstring s) { _edFileName.text = s; return this; }
1227     /// sets widget content text (override to support this)
1228     override @property Widget text(UIString s) { _edFileName.text = s.value; return this; }
1229 
1230     /// mapping of file extension to icon resource name, e.g. ".txt": "text-plain"
1231     @property ref string[string] filetypeIcons() { return _filetypeIcons; }
1232 
1233     /// filter list for file type filter combo box
1234     @property FileFilterEntry[] filters() {
1235         return _filters;
1236     }
1237 
1238     /// filter list for file type filter combo box
1239     @property void filters(FileFilterEntry[] values) {
1240         _filters = values;
1241     }
1242 
1243     /// add new filter entry
1244     void addFilter(FileFilterEntry value) {
1245         _filters ~= value;
1246     }
1247 
1248     /// filter index
1249     @property int filterIndex() {
1250         return _filterIndex;
1251     }
1252 
1253     /// filter index
1254     @property void filterIndex(int index) {
1255         _filterIndex = index;
1256     }
1257 
1258     @property bool readOnly() { return _edFileName.readOnly; }
1259     @property void readOnly(bool f) { _edFileName.readOnly = f; }
1260 
1261 }
1262 
1263 class DirEditLine : FileNameEditLine {
1264     this(string ID = null) {
1265         super(ID);
1266         _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable
1267             | FileDialogFlag.FileMustExist | FileDialogFlag.SelectDirectory | FileDialogFlag.EnableCreateDirectory;
1268         _caption = UIString.fromId("ACTION_SELECT_DIRECTORY"c);
1269     }
1270 }
1271 
1272 //import dlangui.widgets.metadata;
1273 //mixin(registerWidgets!(FileNameEditLine, DirEditLine)());