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 private import std.array : split;
49 
50 
51 /// flags for file dialog options
52 enum FileDialogFlag : uint {
53     /// file must exist (use this for open dialog)
54     FileMustExist = 0x100,
55     /// ask before saving to existing
56     ConfirmOverwrite = 0x200,
57     /// select directory, not file
58     SelectDirectory = 0x400,
59     /// show Create Directory button
60     EnableCreateDirectory = 0x800,
61     /// flags for Open dialog
62     Open = FileMustExist | EnableCreateDirectory,
63     /// flags for Save dialog
64     Save = ConfirmOverwrite | EnableCreateDirectory,
65 
66 }
67 
68 /// File dialog action codes
69 enum FileDialogActions : int {
70     ShowInFileManager = 4000,
71     CreateDirectory = 4001,
72     DeleteFile = 4002,
73 }
74 
75 /// filetype filter entry for FileDialog
76 struct FileFilterEntry {
77     UIString label;
78     string[] filter;
79     bool executableOnly;
80     this(UIString displayLabel, string filterList, bool executableOnly = false) {
81         label = displayLabel;
82         if (filterList.length)
83             filter = split(filterList, ";");
84         this.executableOnly = executableOnly;
85     }
86 }
87 
88 
89 /// File open / save dialog
90 class FileDialog : Dialog, CustomGridCellAdapter {
91     protected FilePathPanel _edPath;
92     protected EditLine _edFilename;
93     protected ComboBox _cbFilters;
94     protected StringGridWidget _fileList;
95     protected Widget leftPanel;
96     protected VerticalLayout rightPanel;
97     protected Action _action;
98 
99     protected RootEntry[] _roots;
100     protected FileFilterEntry[] _filters;
101     protected int _filterIndex;
102     protected string _path;
103     protected string _filename;
104     protected DirEntry[] _entries;
105     protected bool _isRoot;
106 
107     protected bool _isOpenDialog;
108 
109     protected bool _showHiddenFiles;
110     protected bool _allowMultipleFiles;
111 
112     protected string[string] _filetypeIcons;
113 
114     this(UIString caption, Window parent, Action action = null, uint fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist) {
115         super(caption, parent, fileDialogFlags | (Platform.instance.uiDialogDisplayMode & DialogDisplayMode.fileDialogInPopup ? DialogFlag.Popup : 0));
116         _isOpenDialog = !(_flags & FileDialogFlag.ConfirmOverwrite);
117         if (action is null) {
118             if (fileDialogFlags & FileDialogFlag.SelectDirectory)
119                 action = ACTION_OPEN_DIRECTORY.clone();
120             else if (_isOpenDialog)
121                 action = ACTION_OPEN.clone();
122             else
123                 action = ACTION_SAVE.clone();
124         }
125         _action = action;
126     }
127 
128     /// mapping of file extension to icon resource name, e.g. ".txt": "text-plain"
129     @property ref string[string] filetypeIcons() { return _filetypeIcons; }
130 
131     /// filter list for file type filter combo box
132     @property FileFilterEntry[] filters() {
133         return _filters;
134     }
135 
136     /// filter list for file type filter combo box
137     @property void filters(FileFilterEntry[] values) {
138         _filters = values;
139     }
140 
141     /// add new filter entry
142     void addFilter(FileFilterEntry value) {
143         _filters ~= value;
144     }
145 
146     /// filter index
147     @property int filterIndex() {
148         return _filterIndex;
149     }
150 
151     /// filter index
152     @property void filterIndex(int index) {
153         _filterIndex = index;
154     }
155 
156     /// the path to the directory whose files should be displayed
157     @property string path() {
158         return _path;
159     }
160 
161     @property void path(string s) {
162         _path = s;
163     }
164 
165     /// the name of the file or directory that is currently selected
166     @property string filename() {
167         return _filename;
168     }
169 
170     @property void filename(string s) {
171         _filename = s;
172     }
173 
174     /// all the selected filenames
175     @property string[] filenames() {
176         string[] res;
177         res.reserve(_fileList.selection.length);
178         int i = 0;
179         foreach (val; _fileList.selection) {
180             res ~= _entries[val.y];
181             ++i;
182         }
183         return res;
184     }
185 
186     @property bool showHiddenFiles() {
187         return _showHiddenFiles;
188     }
189 
190     @property void showHiddenFiles(bool b) {
191         _showHiddenFiles = b;
192     }
193 
194     @property bool allowMultipleFiles() {
195         return _allowMultipleFiles;
196     }
197 
198     @property void allowMultipleFiles(bool b) {
199         _allowMultipleFiles = b;
200     }
201     
202     /// return currently selected filter value - array of patterns like ["*.txt", "*.rtf"]
203     @property string[] selectedFilter() {
204         if (_filterIndex >= 0 && _filterIndex < _filters.length)
205             return _filters[_filterIndex].filter;
206         return null;
207     }
208 
209     @property bool executableFilterSelected() {
210         if (_filterIndex >= 0 && _filterIndex < _filters.length)
211             return _filters[_filterIndex].executableOnly;
212         return false;
213     }
214 
215     protected bool upLevel() {
216         return openDirectory(parentDir(_path), _path);
217     }
218 
219     protected bool reopenDirectory() {
220         return openDirectory(_path, null);
221     }
222 
223     protected void locateFileInList(dstring pattern) {
224         if (!pattern.length)
225             return;
226         int selection = _fileList.row;
227         if (selection < 0)
228             selection = 0;
229         int index = -1; // first matched item
230         string mask = pattern.toUTF8;
231         // search forward from current row to end of list
232         for(int i = selection; i < _entries.length; i++) {
233             string fname = baseName(_entries[i].name);
234             if (fname.startsWith(mask)) {
235                 index = i;
236                 break;
237             }
238         }
239         if (index < 0) {
240             // search from beginning of list to current position
241             for(int i = 0; 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         }
249         if (index >= 0) {
250             // move selection
251             _fileList.selectCell(1, index + 1);
252             window.update();
253         }
254     }
255 
256     protected bool openDirectory(string dir, string selectedItemPath) {
257         dir = buildNormalizedPath(dir);
258         Log.d("FileDialog.openDirectory(", dir, ")");
259         DirEntry[] entries;
260 
261         auto attrFilter = (showHiddenFiles ? AttrFilter.all : AttrFilter.allVisible) | AttrFilter.special | AttrFilter.parent;
262         if (executableFilterSelected()) {
263             attrFilter |= AttrFilter.executable;
264         }
265         try {
266             _entries = listDirectory(dir, attrFilter, selectedFilter());
267         } catch(Exception e) {
268             import dlangui.dialogs.msgbox;
269             auto msgBox = new MessageBox(UIString.fromRaw("Error"d), UIString.fromRaw(e.msg.toUTF32), window());
270             msgBox.show();
271             return false;
272         }
273         _fileList.rows = 0;
274         _path = dir;
275         _isRoot = isRoot(dir);
276         _edPath.path = _path; //toUTF32(_path);
277         _fileList.rows = cast(int)_entries.length;
278         int selectionIndex = -1;
279         for (int i = 0; i < _entries.length; i++) {
280             if (_entries[i].name.equal(selectedItemPath))
281                 selectionIndex = i;
282             string fname = baseName(_entries[i].name);
283             string sz;
284             string date;
285             bool d = _entries[i].isDir;
286             _fileList.setCellText(1, i, toUTF32(fname));
287             if (d) {
288                 _fileList.setCellText(0, i, "folder");
289             } else {
290                 string ext = extension(fname);
291                 string resname;
292                 if (ext in _filetypeIcons)
293                     resname = _filetypeIcons[ext];
294                 else if (baseName(fname) in _filetypeIcons)
295                     resname = _filetypeIcons[baseName(fname)];
296                 else
297                     resname = "text-plain";
298                 _fileList.setCellText(0, i, toUTF32(resname));
299                 double size = _entries[i].size;
300                 import std.format : format;
301                 sz = size < 1024 ? to!string(size) ~ " B" :
302                     (size < 1024*1024 ? "%.1f".format(size/1024) ~ " KB" :
303                     (size < 1024*1024*1024 ? "%.1f".format(size/(1024*1024)) ~ " MB" :
304                     "%.1f".format(size/(1024*1024*1024)) ~ " GB"));
305                 import std.datetime;
306                 SysTime ts = _entries[i].timeLastModified;
307                 //string timeString = "%04d.%02d.%02d %02d:%02d:%02d".format(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second);
308                 string timeString = "%04d.%02d.%02d %02d:%02d".format(ts.year, ts.month, ts.day, ts.hour, ts.minute);
309                 date = timeString;
310             }
311             _fileList.setCellText(2, i, toUTF32(sz));
312             _fileList.setCellText(3, i, toUTF32(date));
313         }
314         if(_fileList.height > 0)
315             _fileList.scrollTo(0, 0);
316 
317         autofitGrid();
318         if (selectionIndex >= 0)
319             _fileList.selectCell(1, selectionIndex + 1, true);
320         else if (_entries.length > 0)
321             _fileList.selectCell(1, 1, true);
322         return true;
323     }
324 
325     void autofitGrid() {
326         _fileList.autoFitColumnWidths();
327         //_fileList.setColWidth(1, 0);
328         _fileList.fillColumnWidth(1);
329     }
330 
331     override bool onKeyEvent(KeyEvent event) {
332         if (event.action == KeyAction.KeyDown) {
333             if (event.keyCode == KeyCode.BACK && event.flags == 0) {
334                 upLevel();
335                 return true;
336             }
337         }
338         return super.onKeyEvent(event);
339     }
340 
341     /// return true for custom drawn cell
342     override bool isCustomCell(int col, int row) {
343         if ((col == 0 || col == 1) && row >= 0)
344             return true;
345         return false;
346     }
347 
348     protected DrawableRef rowIcon(int row) {
349         string iconId = toUTF8(_fileList.cellText(0, row));
350         DrawableRef res;
351         if (iconId.length)
352             res = drawableCache.get(iconId);
353         return res;
354     }
355 
356     /// return cell size
357     override Point measureCell(int col, int row) {
358         if (col == 1) {
359             FontRef fnt = _fileList.font;
360             dstring txt = _fileList.cellText(col, row);
361             Point sz = fnt.textSize(txt);
362             if (sz.y < fnt.height)
363                 sz.y = fnt.height;
364             return sz;
365         }
366         if (BACKEND_CONSOLE)
367             return Point(0, 0);
368         DrawableRef icon = rowIcon(row);
369         if (icon.isNull)
370             return Point(0, 0);
371         return Point(icon.width + 2.pointsToPixels, icon.height + 2.pointsToPixels);
372     }
373 
374     /// draw data cell content
375     override void drawCell(DrawBuf buf, Rect rc, int col, int row) {
376         if (col == 1) {
377             if (BACKEND_GUI) 
378                 rc.shrink(2, 1);
379             else 
380                 rc.right--;
381             FontRef fnt = _fileList.font;
382             dstring txt = _fileList.cellText(col, row);
383             Point sz = fnt.textSize(txt);
384             Align ha = Align.Left;
385             //if (sz.y < rc.height)
386             //    applyAlign(rc, sz, ha, Align.VCenter);
387             int offset = BACKEND_CONSOLE ? 0 : 1;
388             uint cl = _fileList.textColor;
389             if (_entries[row].isDir)
390                 cl = style.customColor("file_dialog_dir_name_color", cl);
391             fnt.drawText(buf, rc.left + offset, rc.top + offset, txt, cl);
392             return;
393         }
394         DrawableRef img = rowIcon(row);
395         if (!img.isNull) {
396             Point sz;
397             sz.x = img.width;
398             sz.y = img.height;
399             applyAlign(rc, sz, Align.HCenter, Align.VCenter);
400             uint st = state;
401             img.drawTo(buf, rc, st);
402         }
403     }
404 
405     protected ListWidget createRootsList() {
406         ListWidget res = new ListWidget("ROOTS_LIST");
407         res.styleId = STYLE_LIST_BOX;
408         WidgetListAdapter adapter = new WidgetListAdapter();
409         foreach(ref RootEntry root; _roots) {
410             ImageTextButton btn = new ImageTextButton(null, root.icon, root.label);
411             static if (BACKEND_CONSOLE) btn.margins = Rect(1, 1, 0, 0);
412             btn.orientation = Orientation.Vertical;
413             btn.styleId = STYLE_TRANSPARENT_BUTTON_BACKGROUND;
414             btn.focusable = false;
415             btn.tooltipText = root.path.toUTF32;
416             adapter.add(btn);
417         }
418         res.ownAdapter = adapter;
419         res.layoutWidth(WRAP_CONTENT).layoutHeight(FILL_PARENT).layoutWeight(0);
420         res.itemClick = delegate(Widget source, int itemIndex) {
421             openDirectory(_roots[itemIndex].path, null);
422             res.selectItem(-1);
423             return true;
424         };
425         res.focusable = true;
426         debug Log.d("root lisk styleId=", res.styleId);
427         return res;
428     }
429 
430     /// file list item activated (double clicked or Enter key pressed)
431     protected void onItemActivated(int index) {
432         DirEntry e = _entries[index];
433         if (e.isDir) {
434             openDirectory(e.name, _path);
435         } else if (e.isFile) {
436             string fname = e.name;
437             Action result = _action;
438             result.stringParam = fname;
439             close(result);
440         }
441 
442     }
443 
444     /// file list item selected
445     protected void onItemSelected(int index) {
446         DirEntry e = _entries[index];
447         string fname = e.name;
448         _edFilename.text = toUTF32(baseName(fname));
449         _filename = fname;
450     }
451 
452     protected void createAndEnterDirectory(string name) {
453         string newdir = buildNormalizedPath(_path, name);
454         try {
455             mkdirRecurse(newdir);
456             openDirectory(newdir, null);
457         } catch (Exception e) {
458             window.showMessageBox(UIString.fromId("CREATE_FOLDER_ERROR_TITLE"c), UIString.fromId("CREATE_FOLDER_ERROR_MESSAGE"c));
459         }
460     }
461 
462     /// Custom handling of actions
463     override bool handleAction(const Action action) {
464         if (action.id == StandardAction.Cancel) {
465             super.handleAction(action);
466             return true;
467         }
468         if (action.id == FileDialogActions.ShowInFileManager) {
469             Platform.instance.showInFileManager(action.stringParam);
470             return true;
471         }
472         if (action.id == StandardAction.CreateDirectory) {
473             // show editor popup
474             window.showInputBox(UIString.fromId("CREATE_NEW_FOLDER"c), UIString.fromId("INPUT_NAME_FOR_FOLDER"c), ""d, delegate(dstring s) {
475                 if (!s.empty)
476                     createAndEnterDirectory(toUTF8(s));
477             });
478             return true;
479         }
480         if (action.id == StandardAction.Open || action.id == StandardAction.OpenDirectory || action.id == StandardAction.Save) {
481             auto baseFilename = toUTF8(_edFilename.text);
482             _filename = _path ~ dirSeparator ~ baseFilename;
483             
484             if (action.id != StandardAction.OpenDirectory && exists(_filename) && isDir(_filename)) {
485                 auto row = _fileList.row();
486                 onItemActivated(row);
487                 return true;
488             } else if (baseFilename.length > 0) {
489                 Action result = _action;
490                 result.stringParam = _filename;
491                 // success if either selected dir & has to open dir or if selected file
492                 if (action.id == StandardAction.OpenDirectory && exists(_filename) && isDir(_filename) || 
493                     action.id == StandardAction.Save && !(_flags & FileDialogFlag.FileMustExist) || 
494                     exists(_filename) && isFile(_filename)) {
495                     close(result);
496                     return true;
497                 }
498             }
499         }
500         return super.handleAction(action);
501     }
502 
503     bool onPathSelected(string path) {
504         //
505         return openDirectory(path, null);
506     }
507 
508     protected MenuItem getCellPopupMenu(GridWidgetBase source, int col, int row) {
509         if (row >= 0 && row < _entries.length) {
510             MenuItem item = new MenuItem();
511             DirEntry e = _entries[row];
512             // show in explorer action
513             auto showAction = new Action(FileDialogActions.ShowInFileManager, "ACTION_FILE_SHOW_IN_FILE_MANAGER"c);
514             showAction.stringParam = e.name;
515             item.add(showAction);
516             // create directory action
517             if (_flags & FileDialogFlag.EnableCreateDirectory)
518                 item.add(ACTION_CREATE_DIRECTORY);
519 
520             if (e.isDir) {
521                 //_edFilename.text = ""d;
522                 //_filename = "";
523             } else if (e.isFile) {
524                 //string fname = e.name;
525                 //_edFilename.text = toUTF32(baseName(fname));
526                 //_filename = fname;
527             }
528             return item;
529         }
530         return null;
531     }
532 
533     /// override to implement creation of dialog controls
534     override void initialize() {
535         _roots = getRootPaths() ~ getBookmarkPaths();
536 
537         layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).minWidth(BACKEND_CONSOLE ? 50 : 600);
538         //minHeight = 400;
539 
540         LinearLayout content = new HorizontalLayout("dlgcontent");
541 
542         content.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); //.minWidth(400).minHeight(300);
543 
544 
545         //leftPanel = new VerticalLayout("places");
546         //leftPanel.addChild(createRootsList());
547         //leftPanel.layoutHeight(FILL_PARENT).minWidth(BACKEND_CONSOLE ? 7 : 40);
548 
549         leftPanel = createRootsList();
550         leftPanel.minWidth(BACKEND_CONSOLE ? 7 : 40.pointsToPixels);
551 
552         rightPanel = new VerticalLayout("main");
553         rightPanel.layoutHeight(FILL_PARENT).layoutWidth(FILL_PARENT);
554         rightPanel.addChild(new TextWidget(null, "Path:"d));
555 
556         content.addChild(leftPanel);
557         content.addChild(rightPanel);
558 
559         _edPath = new FilePathPanel("path");
560         _edPath.layoutWidth(FILL_PARENT);
561         _edPath.layoutWeight = 0;
562         _edPath.onPathSelectionListener = &onPathSelected;
563         HorizontalLayout fnlayout = new HorizontalLayout();
564         fnlayout.layoutWidth(FILL_PARENT);
565         _edFilename = new EditLine("filename");
566         _edFilename.layoutWidth(FILL_PARENT);
567         if (_flags & FileDialogFlag.SelectDirectory) {
568             _edFilename.visibility = Visibility.Gone;
569         }
570 
571         //_edFilename.layoutWeight = 0;
572         fnlayout.addChild(_edFilename);
573         if (_filters.length) {
574             dstring[] filterLabels;
575             foreach(f; _filters)
576                 filterLabels ~= f.label.value;
577             _cbFilters = new ComboBox("filter", filterLabels);
578             _cbFilters.selectedItemIndex = _filterIndex;
579             _cbFilters.itemClick = delegate(Widget source, int itemIndex) {
580                 _filterIndex = itemIndex;
581                 reopenDirectory();
582                 return true;
583             };
584             _cbFilters.layoutWidth(WRAP_CONTENT);
585             _cbFilters.layoutWeight(0);
586             //_cbFilters.backgroundColor = 0xFFC0FF;
587             fnlayout.addChild(_cbFilters);
588             //fnlayout.backgroundColor = 0xFFFFC0;
589         }
590 
591         _fileList = new StringGridWidget("files");
592         _fileList.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
593         _fileList.resize(4, 3);
594         _fileList.setColTitle(0, " "d);
595         _fileList.setColTitle(1, "Name"d);
596         _fileList.setColTitle(2, "Size"d);
597         _fileList.setColTitle(3, "Modified"d);
598         _fileList.showRowHeaders = false;
599         _fileList.rowSelect = true;
600         _fileList.multiSelect = _allowMultipleFiles;
601         _fileList.cellPopupMenu = &getCellPopupMenu;
602         _fileList.menuItemAction = &handleAction;
603 
604         _fileList.keyEvent = delegate(Widget source, KeyEvent event) {
605             if (_shortcutHelper.onKeyEvent(event))
606                 locateFileInList(_shortcutHelper.text);
607             return false;
608         };
609 
610         rightPanel.addChild(_edPath);
611         rightPanel.addChild(_fileList);
612         rightPanel.addChild(fnlayout);
613 
614 
615         addChild(content);
616         if (_flags & FileDialogFlag.EnableCreateDirectory) {
617             addChild(createButtonsPanel([ACTION_CREATE_DIRECTORY, cast(immutable)_action, ACTION_CANCEL], 1, 1));
618         } else {
619             addChild(createButtonsPanel([cast(immutable)_action, ACTION_CANCEL], 0, 0));
620         }
621 
622         _fileList.customCellAdapter = this;
623         _fileList.cellActivated = delegate(GridWidgetBase source, int col, int row) {
624             onItemActivated(row);
625         };
626         _fileList.cellSelected = delegate(GridWidgetBase source, int col, int row) {
627             onItemSelected(row);
628         };
629 
630         if (_path.empty) {
631             _path = currentDir;
632         }
633         openDirectory(_path, _filename);
634         _fileList.layoutHeight = FILL_PARENT;
635 
636     }
637 
638     protected TextTypingShortcutHelper _shortcutHelper;
639 
640     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
641     override void layout(Rect rc) {
642         super.layout(rc);
643         autofitGrid();
644     }
645 
646 
647     ///// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
648     //override void measure(int parentWidth, int parentHeight) { 
649     //    super.measure(parentWidth, parentHeight);
650     //    for(int i = 0; i < childCount; i++) {
651     //        Widget w = child(i);
652     //        Log.d("id=", w.id, " measuredHeight=", w.measuredHeight );
653     //        for (int j = 0; j < w.childCount; j++) {
654     //            Widget w2 = w.child(j);
655     //            Log.d("    id=", w2.id, " measuredHeight=", w.measuredHeight );
656     //        }
657     //    }
658     //    Log.d("this id=", id, " measuredHeight=", measuredHeight);
659     //}
660 
661     override void onShow() {
662         _fileList.setFocus();
663     }
664 }
665 
666 interface OnPathSelectionHandler {
667     bool onPathSelected(string path);
668 }
669 
670 class FilePathPanelItem : HorizontalLayout {
671     protected string _path;
672     protected TextWidget _text;
673     protected ImageButton _button;
674     Listener!OnPathSelectionHandler onPathSelectionListener;
675     this(string path) {
676         super(null);
677         styleId = STYLE_LIST_ITEM;
678         _path = path;
679         string fname = isRoot(path) ? path : baseName(path);
680         _text = new TextWidget(null, toUTF32(fname));
681         _text.styleId = STYLE_BUTTON_TRANSPARENT;
682         _text.clickable = true;
683         _text.click = &onTextClick;
684         //_text.backgroundColor = 0xC0FFFF;
685         _text.state = State.Parent;
686         _button = new ImageButton(null, ATTR_SCROLLBAR_BUTTON_RIGHT);
687         _button.styleId = STYLE_BUTTON_TRANSPARENT;
688         _button.focusable = false;
689         _button.click = &onButtonClick;
690         //_button.backgroundColor = 0xC0FFC0;
691         _button.state = State.Parent;
692         trackHover(true);
693         addChild(_text);
694         addChild(_button);
695         margins(Rect(2.pointsToPixels + 1, 0, 2.pointsToPixels + 1, 0));
696     }
697     private bool onTextClick(Widget src) {
698         if (onPathSelectionListener.assigned)
699             return onPathSelectionListener(_path);
700         return false;
701     }
702     private bool onButtonClick(Widget src) {
703         // show popup menu with subdirs
704         string[] filters;
705         DirEntry[] entries;
706         try {
707             AttrFilter attrFilter = AttrFilter.dirs | AttrFilter.parent;
708             entries = listDirectory(_path, attrFilter);
709         } catch(Exception e) {
710             return false;
711         }
712         if (entries.length == 0)
713             return false;
714         MenuItem dirs = new MenuItem();
715         int itemId = 25000;
716         foreach(ref DirEntry e; entries) {
717             string fullPath = e.name;
718             string d = baseName(fullPath);
719             Action a = new Action(itemId++, toUTF32(d));
720             a.stringParam = fullPath;
721             MenuItem item = new MenuItem(a);
722             item.menuItemAction = delegate(const Action action) {
723                 if (onPathSelectionListener.assigned)
724                     return onPathSelectionListener(action.stringParam);
725                 return false;
726             };
727             dirs.add(item);
728         }
729         PopupMenu menuWidget = new PopupMenu(dirs);
730         PopupWidget popup = window.showPopup(menuWidget, this, PopupAlign.Below);
731         popup.flags = PopupFlags.CloseOnClickOutside;
732         return true;
733     }
734 }
735 
736 /// Panel with buttons - path segments - for fast navigation to subdirs.
737 class FilePathPanelButtons : WidgetGroupDefaultDrawing {
738     protected string _path;
739     Listener!OnPathSelectionHandler onPathSelectionListener;
740     protected bool onPathSelected(string path) {
741         if (onPathSelectionListener.assigned) {
742             return onPathSelectionListener(path);
743         }
744         return false;
745     }
746     this(string ID = null) {
747         super(ID);
748         layoutWidth = FILL_PARENT;
749         clickable = true;
750     }
751     protected void initialize(string path) {
752         _path = path;
753         _children.clear();
754         string itemPath = path;
755         for (;;) {
756             FilePathPanelItem item = new FilePathPanelItem(itemPath);
757             item.onPathSelectionListener = &onPathSelected;
758             addChild(item);
759             if (isRoot(itemPath)) {
760                 break;
761             }
762             itemPath = parentDir(itemPath);
763         }
764     }
765     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
766     override void measure(int parentWidth, int parentHeight) { 
767         Rect m = margins;
768         Rect p = padding;
769         // calc size constraints for children
770         int pwidth = parentWidth;
771         int pheight = parentHeight;
772         if (parentWidth != SIZE_UNSPECIFIED)
773             pwidth -= m.left + m.right + p.left + p.right;
774         if (parentHeight != SIZE_UNSPECIFIED)
775             pheight -= m.top + m.bottom + p.top + p.bottom;
776         int reservedForEmptySpace = parentWidth / 20;
777         if (reservedForEmptySpace > 40.pointsToPixels)
778             reservedForEmptySpace = 40.pointsToPixels;
779         if (reservedForEmptySpace < 4.pointsToPixels)
780             reservedForEmptySpace = 4.pointsToPixels;
781 
782         Point sz;
783         sz.x += reservedForEmptySpace;
784         // measure children
785         bool exceeded = false;
786         for (int i = 0; i < _children.count; i++) {
787             Widget item = _children.get(i);
788             item.visibility = Visibility.Visible;
789             item.measure(pwidth, pheight);
790             if (sz.y < item.measuredHeight)
791                 sz.y = item.measuredHeight;
792             if (sz.x + item.measuredWidth > pwidth) {
793                 exceeded = true;
794             }
795             if (!exceeded || i == 0) // at least one item must be visible
796                 sz.x += item.measuredWidth;
797             else
798                 item.visibility = Visibility.Gone;
799         }
800         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
801     }
802     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
803     override void layout(Rect rc) {
804         //Log.d("tabControl.layout enter");
805         _needLayout = false;
806         if (visibility == Visibility.Gone) {
807             return;
808         }
809         _pos = rc;
810         applyMargins(rc);
811         applyPadding(rc);
812 
813         int reservedForEmptySpace = rc.width / 20;
814         if (reservedForEmptySpace > 40)
815             reservedForEmptySpace = 40;
816         if (reservedForEmptySpace < 4)
817             reservedForEmptySpace = 4;
818         int maxw = rc.width - reservedForEmptySpace;
819         int totalw = 0;
820         int visibleItems = 0;
821         bool exceeded = false;
822         // measure and update visibility
823         for (int i = 0; i < _children.count; i++) {
824             Widget item = _children.get(i);
825             item.visibility = Visibility.Visible;
826             item.measure(rc.width, rc.height);
827             if (totalw + item.measuredWidth > rc.width) {
828                 exceeded = true;
829             }
830             if (!exceeded || i == 0) { // at least one item must be visible
831                 totalw += item.measuredWidth;
832                 visibleItems++;
833             } else
834                 item.visibility = Visibility.Gone;
835         }
836         // layout visible items
837         // backward order
838         Rect itemRect = rc;
839         for (int i = visibleItems - 1; i >= 0; i--) {
840             Widget item = _children.get(i);
841             int w = item.measuredWidth;
842             if (i == visibleItems - 1 && w > maxw)
843                 w = maxw;
844             itemRect.right = itemRect.left + w;
845             item.layout(itemRect);
846             itemRect.left += w;
847         }
848 
849     }
850 
851 
852 }
853 
854 interface PathSelectedHandler {
855     bool onPathSelected(string path);
856 }
857 
858 /// Panel - either path segment buttons or text editor line
859 class FilePathPanel : FrameLayout {
860     Listener!OnPathSelectionHandler onPathSelectionListener;
861     static const ID_SEGMENTS = "SEGMENTS";
862     static const ID_EDITOR = "ED_PATH";
863     protected FilePathPanelButtons _segments;
864     protected EditLine _edPath;
865     protected string _path;
866     Signal!PathSelectedHandler pathListener;
867     this(string ID = null) {
868         super(ID);
869         _segments = new FilePathPanelButtons(ID_SEGMENTS);
870         _edPath = new EditLine(ID_EDITOR);
871         _edPath.layoutWidth = FILL_PARENT;
872         _edPath.editorAction = &onEditorAction;
873         _edPath.focusChange = &onEditorFocusChanged;
874         _segments.click = &onSegmentsClickOutside;
875         _segments.onPathSelectionListener = &onPathSelected;
876         addChild(_segments);
877         addChild(_edPath);
878     }
879     protected bool onEditorFocusChanged(Widget source, bool focused) {
880         if (!focused) {
881             _edPath.text = toUTF32(_path);
882             showChild(ID_SEGMENTS);
883         }
884         return true;
885     }
886     protected bool onPathSelected(string path) {
887         if (onPathSelectionListener.assigned) {
888             if (exists(path))
889                 return onPathSelectionListener(path);
890         }
891         return false;
892     }
893     protected bool onSegmentsClickOutside(Widget w) {
894         // switch to editor
895         _edPath.text = toUTF32(_path);
896         showChild(ID_EDITOR);
897         _edPath.setFocus();
898         return true;
899     }
900     protected bool onEditorAction(const Action action) {
901         if (action.id == EditorActions.InsertNewLine) {
902             string fn = buildNormalizedPath(toUTF8(_edPath.text));
903             if (exists(fn) && isDir(fn))
904                 return onPathSelected(fn);
905         }
906         return false;
907     }
908 
909     @property void path(string value) {
910         _segments.initialize(value);
911         _edPath.text = toUTF32(value);
912         _path = value;
913         showChild(ID_SEGMENTS);
914     }
915     @property string path() {
916         return _path;
917     }
918 }
919 
920 class FileNameEditLine : HorizontalLayout {
921     protected EditLine _edFileName;
922     protected Button _btn;
923     protected string[string] _filetypeIcons;
924     protected dstring _caption = "Open File"d;
925     protected uint _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist | FileDialogFlag.EnableCreateDirectory;
926     protected FileFilterEntry[] _filters;
927     protected int _filterIndex;
928 
929     /// Modified state change listener (e.g. content has been saved, or first time modified after save)
930     Signal!ModifiedStateListener modifiedStateChange;
931     /// editor content is changed
932     Signal!EditableContentChangeListener contentChange;
933 
934     this(string ID = null) {
935         super(ID);
936         _edFileName = new EditLine("FileNameEditLine_edFileName");
937         _edFileName.minWidth(BACKEND_CONSOLE ? 16 : 200);
938         _btn = new Button("FileNameEditLine_btnFile", "..."d);
939         _btn.styleId = STYLE_BUTTON_NOMARGINS;
940         _btn.layoutWeight = 0;
941         _btn.click = delegate(Widget src) {
942             FileDialog dlg = new FileDialog(UIString.fromRaw(_caption), window, null, _fileDialogFlags);
943             foreach(key, value; _filetypeIcons)
944                 dlg.filetypeIcons[key] = value;
945             dlg.filters = _filters;
946             dlg.dialogResult = delegate(Dialog dlg, const Action result) {
947                 if (result.id == ACTION_OPEN.id || result.id == ACTION_OPEN_DIRECTORY.id) {
948                     _edFileName.text = toUTF32(result.stringParam);
949                     if (contentChange.assigned)
950                         contentChange(_edFileName.content);
951                 }
952             };
953             string path = toUTF8(_edFileName.text);
954             if (!path.empty) {
955                 if (exists(path) && isFile(path)) {
956                     dlg.path = dirName(path);
957                     dlg.filename = baseName(path);
958                 } else if (exists(path) && isDir(path)) {
959                     dlg.path = path;
960                 }
961             }
962             dlg.show();
963             return true;
964         };
965         _edFileName.contentChange = delegate(EditableContent content) {
966             if (contentChange.assigned)
967                 contentChange(content);
968         };
969         _edFileName.modifiedStateChange = delegate(Widget src, bool modified) {
970             if (modifiedStateChange.assigned)
971                 modifiedStateChange(src, modified);
972         };
973         addChild(_edFileName);
974         addChild(_btn);
975     }
976 
977     @property uint fileDialogFlags() { return _fileDialogFlags; }
978     @property void fileDialogFlags(uint f) { _fileDialogFlags = f; }
979 
980     @property dstring caption() { return _caption; }
981     @property void caption(dstring s) { _caption = s; }
982 
983     /// returns widget content text (override to support this)
984     override @property dstring text() { return _edFileName.text; }
985     /// sets widget content text (override to support this)
986     override @property Widget text(dstring s) { _edFileName.text = s; return this; }
987     /// sets widget content text (override to support this)
988     override @property Widget text(UIString s) { _edFileName.text = s.value; return this; }
989 
990     /// mapping of file extension to icon resource name, e.g. ".txt": "text-plain"
991     @property ref string[string] filetypeIcons() { return _filetypeIcons; }
992 
993     /// filter list for file type filter combo box
994     @property FileFilterEntry[] filters() {
995         return _filters;
996     }
997 
998     /// filter list for file type filter combo box
999     @property void filters(FileFilterEntry[] values) {
1000         _filters = values;
1001     }
1002 
1003     /// add new filter entry
1004     void addFilter(FileFilterEntry value) {
1005         _filters ~= value;
1006     }
1007 
1008     /// filter index
1009     @property int filterIndex() {
1010         return _filterIndex;
1011     }
1012 
1013     /// filter index
1014     @property void filterIndex(int index) {
1015         _filterIndex = index;
1016     }
1017 
1018     @property bool readOnly() { return _edFileName.readOnly; }
1019     @property void readOnly(bool f) { _edFileName.readOnly = f; }
1020 
1021 }
1022 
1023 class DirEditLine : FileNameEditLine {
1024     this(string ID = null) {
1025         super(ID);
1026         _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable 
1027             | FileDialogFlag.FileMustExist | FileDialogFlag.SelectDirectory | FileDialogFlag.EnableCreateDirectory;
1028         _caption = "Select directory"d;
1029     }
1030 }
1031 
1032 //import dlangui.widgets.metadata;
1033 //mixin(registerWidgets!(FileNameEditLine, DirEditLine)());