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)());