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 //date = "%04d.%02d.%02d %02d:%02d:%02d".format(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second); 377 return "%04d.%02d.%02d %02d:%02d".format(ts.year, ts.month, ts.day, ts.hour, ts.minute); 378 } 379 } 380 381 protected int entriesToCells(string selectedItemPath) { 382 _fileList.rows = cast(int)_entries.length; 383 int selectionIndex = -1; 384 for (int i = 0; i < _entries.length; i++) { 385 if (_entries[i].name.equal(selectedItemPath)) 386 selectionIndex = i; 387 string fname = baseName(_entries[i].name); 388 string sz; 389 string date; 390 bool d = _entries[i].isDir; 391 _fileList.setCellText(1, i, toUTF32(fname)); 392 if (d) { 393 _fileList.setCellText(0, i, "folder"); 394 if (fname != "..") 395 date = formatTimestamp(_entries[i]); 396 } else { 397 string ext = extension(fname); 398 string resname; 399 if (ext in _filetypeIcons) 400 resname = _filetypeIcons[ext]; 401 else if (baseName(fname) in _filetypeIcons) 402 resname = _filetypeIcons[baseName(fname)]; 403 else 404 resname = "text-plain"; 405 _fileList.setCellText(0, i, toUTF32(resname)); 406 double size = double.nan; 407 try { 408 size = _entries[i].size; 409 } catch (Exception e) { 410 Log.w(e.msg); 411 } 412 import std.math : isNaN; 413 if (size.isNaN) 414 sz = "--"; 415 else { 416 import std.format : format; 417 sz = size < 1024 ? to!string(size) ~ " B" : 418 (size < 1024*1024 ? "%.1f".format(size/1024) ~ " KB" : 419 (size < 1024*1024*1024 ? "%.1f".format(size/(1024*1024)) ~ " MB" : 420 "%.1f".format(size/(1024*1024*1024)) ~ " GB")); 421 } 422 date = formatTimestamp(_entries[i]); 423 } 424 _fileList.setCellText(2, i, toUTF32(sz)); 425 _fileList.setCellText(3, i, toUTF32(date)); 426 } 427 if(_fileList.height > 0) 428 _fileList.scrollTo(0, 0); 429 430 autofitGrid(); 431 if (selectionIndex >= 0) 432 _fileList.selectCell(1, selectionIndex + 1, true); 433 else if (_entries.length > 0) 434 _fileList.selectCell(1, 1, true); 435 return selectionIndex; 436 } 437 438 protected bool openDirectory(string dir, string selectedItemPath) { 439 dir = buildNormalizedPath(dir); 440 Log.d("FileDialog.openDirectory(", dir, ")"); 441 DirEntry[] entries; 442 443 auto attrFilter = (showHiddenFiles ? AttrFilter.all : AttrFilter.allVisible) | AttrFilter.special | AttrFilter.parent; 444 if (executableFilterSelected()) { 445 attrFilter |= AttrFilter.executable; 446 } 447 try { 448 _entries = listDirectory(dir, attrFilter, selectedFilter()); 449 } catch(Exception e) { 450 Log.e("Cannot list directory " ~ dir, e); 451 //import dlangui.dialogs.msgbox; 452 //auto msgBox = new MessageBox(UIString.fromId("MESSAGE_ERROR"c), UIString.fromRaw(e.msg.toUTF32), window()); 453 //msgBox.show(); 454 //return false; 455 // show empty dir if failed to read 456 } 457 _fileList.rows = 0; 458 _path = dir; 459 _isRoot = isRoot(dir); 460 _edPath.path = _path; //toUTF32(_path); 461 int selectionIndex = entriesToCells(selectedItemPath); 462 return true; 463 } 464 465 void autofitGrid() { 466 _fileList.autoFitColumnWidths(); 467 //_fileList.setColWidth(1, 0); 468 _fileList.fillColumnWidth(1); 469 } 470 471 override bool onKeyEvent(KeyEvent event) { 472 if (event.action == KeyAction.KeyDown) { 473 if (event.keyCode == KeyCode.BACK && event.flags == 0) { 474 upLevel(); 475 return true; 476 } 477 } 478 return super.onKeyEvent(event); 479 } 480 481 /// return true for custom drawn cell 482 override bool isCustomCell(int col, int row) { 483 if ((col == 0 || col == 1) && row >= 0) 484 return true; 485 return false; 486 } 487 488 protected DrawableRef rowIcon(int row) { 489 string iconId = toUTF8(_fileList.cellText(0, row)); 490 DrawableRef res; 491 if (iconId.length) 492 res = drawableCache.get(iconId); 493 return res; 494 } 495 496 /// return cell size 497 override Point measureCell(int col, int row) { 498 if (col == 1) { 499 FontRef fnt = _fileList.font; 500 dstring txt = _fileList.cellText(col, row); 501 Point sz = fnt.textSize(txt); 502 if (sz.y < fnt.height) 503 sz.y = fnt.height; 504 return sz; 505 } 506 if (WIDGET_STYLE_CONSOLE) 507 return Point(0, 0); 508 else { 509 DrawableRef icon = rowIcon(row); 510 if (icon.isNull) 511 return Point(0, 0); 512 return Point(icon.width + 2.pointsToPixels, icon.height + 2.pointsToPixels); 513 } 514 } 515 516 /// draw data cell content 517 override void drawCell(DrawBuf buf, Rect rc, int col, int row) { 518 if (col == 1) { 519 if (BACKEND_GUI) 520 rc.shrink(2, 1); 521 else 522 rc.right--; 523 FontRef fnt = _fileList.font; 524 dstring txt = _fileList.cellText(col, row); 525 Point sz = fnt.textSize(txt); 526 Align ha = Align.Left; 527 //if (sz.y < rc.height) 528 // applyAlign(rc, sz, ha, Align.VCenter); 529 int offset = WIDGET_STYLE_CONSOLE ? 0 : 1; 530 uint cl = _fileList.textColor; 531 if (_entries[row].isDir) 532 cl = style.customColor("file_dialog_dir_name_color", cl); 533 fnt.drawText(buf, rc.left + offset, rc.top + offset, txt, cl); 534 return; 535 } 536 DrawableRef img = rowIcon(row); 537 if (!img.isNull) { 538 Point sz; 539 sz.x = img.width; 540 sz.y = img.height; 541 applyAlign(rc, sz, Align.HCenter, Align.VCenter); 542 uint st = state; 543 img.drawTo(buf, rc, st); 544 } 545 } 546 547 protected ListWidget createRootsList() { 548 ListWidget res = new ListWidget("ROOTS_LIST"); 549 res.styleId = STYLE_LIST_BOX; 550 WidgetListAdapter adapter = new WidgetListAdapter(); 551 foreach(ref RootEntry root; _roots) { 552 ImageTextButton btn = new ImageTextButton(null, root.icon, root.label); 553 static if (WIDGET_STYLE_CONSOLE) btn.margins = Rect(1, 1, 0, 0); 554 btn.orientation = Orientation.Vertical; 555 btn.styleId = STYLE_TRANSPARENT_BUTTON_BACKGROUND; 556 btn.focusable = false; 557 btn.tooltipText = root.path.toUTF32; 558 adapter.add(btn); 559 } 560 res.ownAdapter = adapter; 561 res.layoutWidth(WRAP_CONTENT).layoutHeight(FILL_PARENT).layoutWeight(0); 562 res.itemClick = delegate(Widget source, int itemIndex) { 563 openDirectory(_roots[itemIndex].path, null); 564 res.selectItem(-1); 565 return true; 566 }; 567 res.focusable = true; 568 debug Log.d("root list styleId=", res.styleId); 569 return res; 570 } 571 572 /// file list item activated (double clicked or Enter key pressed) 573 protected void onItemActivated(int index) { 574 DirEntry e = _entries[index]; 575 if (e.isDir) { 576 openDirectory(e.name, _path); 577 } else if (e.isFile) { 578 string fname = e.name; 579 if ((_flags & FileDialogFlag.ConfirmOverwrite) && exists(fname) && isFile(fname)) { 580 showConfirmOverwriteQuestion(fname); 581 return; 582 } 583 else { 584 Action result = _action; 585 result.stringParam = fname; 586 close(result); 587 } 588 } 589 } 590 591 /// file list item selected 592 protected void onItemSelected(int index) { 593 DirEntry e = _entries[index]; 594 string fname = e.name; 595 _edFilename.text = toUTF32(baseName(fname)); 596 _filename = fname; 597 } 598 599 protected void createAndEnterDirectory(string name) { 600 string newdir = buildNormalizedPath(_path, name); 601 try { 602 mkdirRecurse(newdir); 603 openDirectory(newdir, null); 604 } catch (Exception e) { 605 window.showMessageBox(UIString.fromId("CREATE_FOLDER_ERROR_TITLE"c), UIString.fromId("CREATE_FOLDER_ERROR_MESSAGE"c)); 606 } 607 } 608 609 /// calls close with default action; returns true if default action is found and invoked 610 override protected bool closeWithDefaultAction() { 611 return handleAction(_action); 612 } 613 614 /// Custom handling of actions 615 override bool handleAction(const Action action) { 616 if (action.id == StandardAction.Cancel) { 617 super.handleAction(action); 618 return true; 619 } 620 if (action.id == FileDialogActions.ShowInFileManager) { 621 Platform.instance.showInFileManager(action.stringParam); 622 return true; 623 } 624 if (action.id == StandardAction.CreateDirectory) { 625 // show editor popup 626 window.showInputBox(UIString.fromId("CREATE_NEW_FOLDER"c), UIString.fromId("INPUT_NAME_FOR_FOLDER"c), ""d, delegate(dstring s) { 627 if (!s.empty) 628 createAndEnterDirectory(toUTF8(s)); 629 }); 630 return true; 631 } 632 if (action.id == StandardAction.Open || action.id == StandardAction.OpenDirectory || action.id == StandardAction.Save) { 633 auto baseFilename = toUTF8(_edFilename.text); 634 if (action.id == StandardAction.OpenDirectory) 635 _filename = _path ~ dirSeparator; 636 else 637 _filename = _path ~ dirSeparator ~ baseFilename; 638 639 if (action.id != StandardAction.OpenDirectory && exists(_filename) && isDir(_filename)) { 640 // directory name in _edFileName.text but we need file so open directory 641 openDirectory(_filename, null); 642 return true; 643 } else if (baseFilename.length > 0) { 644 Action result = _action; 645 result.stringParam = _filename; 646 // success if either selected dir & has to open dir or if selected file 647 if (action.id == StandardAction.OpenDirectory && exists(_filename) && isDir(_filename)) { 648 close(result); 649 return true; 650 } 651 else if (action.id == StandardAction.Save && !(_flags & FileDialogFlag.FileMustExist)) { 652 // save dialog 653 if ((_flags & FileDialogFlag.ConfirmOverwrite) && exists(_filename) && isFile(_filename)) { 654 showConfirmOverwriteQuestion(_filename); 655 return true; 656 } 657 else { 658 close(result); 659 return true; 660 } 661 } 662 else if (!(_flags & FileDialogFlag.FileMustExist) || (exists(_filename) && isFile(_filename))) { 663 // open dialog 664 close(result); 665 return true; 666 } 667 } 668 } 669 return super.handleAction(action); 670 } 671 672 /// shows question "override file?" 673 protected void showConfirmOverwriteQuestion(string fileName) { 674 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) { 675 if (a.id == StandardAction.Yes) { 676 Action result = _action; 677 result.stringParam = fileName; 678 close(result); 679 } 680 return true; 681 }); 682 } 683 684 bool onPathSelected(string path) { 685 // 686 return openDirectory(path, null); 687 } 688 689 protected MenuItem getCellPopupMenu(GridWidgetBase source, int col, int row) { 690 if (row >= 0 && row < _entries.length) { 691 MenuItem item = new MenuItem(); 692 DirEntry e = _entries[row]; 693 // show in explorer action 694 auto showAction = new Action(FileDialogActions.ShowInFileManager, "ACTION_FILE_SHOW_IN_FILE_MANAGER"c); 695 showAction.stringParam = e.name; 696 item.add(showAction); 697 // create directory action 698 if (_flags & FileDialogFlag.EnableCreateDirectory) 699 item.add(ACTION_CREATE_DIRECTORY); 700 701 if (e.isDir) { 702 //_edFilename.text = ""d; 703 //_filename = ""; 704 } else if (e.isFile) { 705 //string fname = e.name; 706 //_edFilename.text = toUTF32(baseName(fname)); 707 //_filename = fname; 708 } 709 return item; 710 } 711 return null; 712 } 713 714 /// override to implement creation of dialog controls 715 override void initialize() { 716 // remember filename specified by user, file grid initialization can change it 717 string defaultFilename = _filename; 718 719 _roots = getRootPaths() ~ getBookmarkPaths(); 720 721 layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).minWidth(WIDGET_STYLE_CONSOLE ? 50 : 600); 722 //minHeight = 400; 723 724 LinearLayout content = new HorizontalLayout("dlgcontent"); 725 726 content.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); //.minWidth(400).minHeight(300); 727 728 729 //leftPanel = new VerticalLayout("places"); 730 //leftPanel.addChild(createRootsList()); 731 //leftPanel.layoutHeight(FILL_PARENT).minWidth(WIDGET_STYLE_CONSOLE ? 7 : 40); 732 733 leftPanel = createRootsList(); 734 leftPanel.minWidth(WIDGET_STYLE_CONSOLE ? 7 : 40.pointsToPixels); 735 736 rightPanel = new VerticalLayout("main"); 737 rightPanel.layoutHeight(FILL_PARENT).layoutWidth(FILL_PARENT); 738 rightPanel.addChild(new TextWidget(null, UIString.fromId("MESSAGE_PATH"c) ~ ":")); 739 740 content.addChild(leftPanel); 741 content.addChild(rightPanel); 742 743 _edPath = new FilePathPanel("path"); 744 _edPath.layoutWidth(FILL_PARENT); 745 _edPath.layoutWeight = 0; 746 _edPath.onPathSelectionListener = &onPathSelected; 747 HorizontalLayout fnlayout = new HorizontalLayout(); 748 fnlayout.layoutWidth(FILL_PARENT); 749 _edFilename = new EditLine("filename"); 750 _edFilename.layoutWidth(FILL_PARENT); 751 _edFilename.setDefaultPopupMenu(); 752 if (_flags & FileDialogFlag.SelectDirectory) { 753 _edFilename.visibility = Visibility.Gone; 754 } 755 756 //_edFilename.layoutWeight = 0; 757 fnlayout.addChild(_edFilename); 758 if (_filters.length) { 759 dstring[] filterLabels; 760 foreach(f; _filters) 761 filterLabels ~= f.label.value; 762 _cbFilters = new ComboBox("filter", filterLabels); 763 _cbFilters.selectedItemIndex = _filterIndex; 764 _cbFilters.itemClick = delegate(Widget source, int itemIndex) { 765 _filterIndex = itemIndex; 766 reopenDirectory(); 767 return true; 768 }; 769 _cbFilters.layoutWidth(WRAP_CONTENT); 770 _cbFilters.layoutWeight(0); 771 //_cbFilters.backgroundColor = 0xFFC0FF; 772 fnlayout.addChild(_cbFilters); 773 //fnlayout.backgroundColor = 0xFFFFC0; 774 } 775 776 _fileList = new StringGridWidget("files"); 777 _fileList.styleId = STYLE_FILE_DIALOG_GRID; 778 _fileList.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); 779 _fileList.fullColumnOnLeft(false); 780 _fileList.fullRowOnTop(false); 781 _fileList.resize(4, 3); 782 _fileList.setColTitle(0, " "d); 783 updateColumnHeaders(); 784 _fileList.showRowHeaders = false; 785 _fileList.rowSelect = true; 786 _fileList.multiSelect = _allowMultipleFiles; 787 _fileList.cellPopupMenu = &getCellPopupMenu; 788 _fileList.menuItemAction = &handleAction; 789 _fileList.minVisibleRows = 10; 790 _fileList.minVisibleCols = 4; 791 _fileList.headerCellClicked = &onHeaderCellClicked; 792 793 _fileList.keyEvent = delegate(Widget source, KeyEvent event) { 794 if (_shortcutHelper.onKeyEvent(event)) 795 locateFileInList(_shortcutHelper.text); 796 return false; 797 }; 798 799 rightPanel.addChild(_edPath); 800 rightPanel.addChild(_fileList); 801 rightPanel.addChild(fnlayout); 802 803 804 addChild(content); 805 if (_flags & FileDialogFlag.EnableCreateDirectory) { 806 addChild(createButtonsPanel([ACTION_CREATE_DIRECTORY, cast(immutable)_action, ACTION_CANCEL], 1, 1)); 807 } else { 808 addChild(createButtonsPanel([cast(immutable)_action, ACTION_CANCEL], 0, 0)); 809 } 810 811 _fileList.customCellAdapter = this; 812 _fileList.cellActivated = delegate(GridWidgetBase source, int col, int row) { 813 onItemActivated(row); 814 }; 815 _fileList.cellSelected = delegate(GridWidgetBase source, int col, int row) { 816 onItemSelected(row); 817 }; 818 819 if (_path.empty || !_path.exists || !_path.isDir) { 820 _path = currentDir; 821 if (!_path.exists || !_path.isDir) 822 _path = homePath; 823 } 824 openDirectory(_path, _filename); 825 _fileList.layoutHeight = FILL_PARENT; 826 827 // set default file name if specified by user 828 if (defaultFilename.length != 0) 829 _edFilename.text = toUTF32(baseName(defaultFilename)); 830 } 831 832 /// get sort order suffix for column title 833 protected dstring appendSortOrderSuffix(dstring columnName, FileListSortOrder arrowUp, FileListSortOrder arrowDown) { 834 if (_sortOrder == arrowUp) 835 return columnName ~ " ▲"; 836 if (_sortOrder == arrowDown) 837 return columnName ~ " ▼"; 838 return columnName; 839 } 840 841 protected void updateColumnHeaders() { 842 _fileList.setColTitle(1, appendSortOrderSuffix(UIString.fromId("COL_NAME"c).value, FileListSortOrder.NAME_DESC, FileListSortOrder.NAME)); 843 _fileList.setColTitle(2, appendSortOrderSuffix(UIString.fromId("COL_SIZE"c).value, FileListSortOrder.SIZE_DESC, FileListSortOrder.SIZE)); 844 _fileList.setColTitle(3, appendSortOrderSuffix(UIString.fromId("COL_MODIFIED"c).value, FileListSortOrder.TIMESTAMP_DESC, FileListSortOrder.TIMESTAMP)); 845 } 846 847 protected void onHeaderCellClicked(GridWidgetBase source, int col, int row) { 848 debug Log.d("onHeaderCellClicked col=", col, " row=", row); 849 if (row == 0 && col >= 2 && col <= 4) { 850 // 2=NAME, 3=SIZE, 4=MODIFIED 851 changeSortOrder(col); 852 } 853 } 854 855 protected TextTypingShortcutHelper _shortcutHelper; 856 857 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 858 override void layout(Rect rc) { 859 super.layout(rc); 860 autofitGrid(); 861 } 862 863 864 ///// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 865 //override void measure(int parentWidth, int parentHeight) { 866 // super.measure(parentWidth, parentHeight); 867 // for(int i = 0; i < childCount; i++) { 868 // Widget w = child(i); 869 // Log.d("id=", w.id, " measuredHeight=", w.measuredHeight ); 870 // for (int j = 0; j < w.childCount; j++) { 871 // Widget w2 = w.child(j); 872 // Log.d(" id=", w2.id, " measuredHeight=", w.measuredHeight ); 873 // } 874 // } 875 // Log.d("this id=", id, " measuredHeight=", measuredHeight); 876 //} 877 878 override void onShow() { 879 _fileList.setFocus(); 880 } 881 } 882 883 interface OnPathSelectionHandler { 884 bool onPathSelected(string path); 885 } 886 887 class FilePathPanelItem : HorizontalLayout { 888 protected string _path; 889 protected TextWidget _text; 890 protected ImageButton _button; 891 Listener!OnPathSelectionHandler onPathSelectionListener; 892 this(string path) { 893 super(null); 894 styleId = STYLE_LIST_ITEM; 895 _path = path; 896 string fname = isRoot(path) ? path : baseName(path); 897 _text = new TextWidget(null, toUTF32(fname)); 898 _text.styleId = STYLE_BUTTON_TRANSPARENT; 899 _text.clickable = true; 900 _text.click = &onTextClick; 901 //_text.backgroundColor = 0xC0FFFF; 902 _text.state = State.Parent; 903 _button = new ImageButton(null, ATTR_SCROLLBAR_BUTTON_RIGHT); 904 _button.styleId = STYLE_BUTTON_TRANSPARENT; 905 _button.focusable = false; 906 _button.click = &onButtonClick; 907 //_button.backgroundColor = 0xC0FFC0; 908 _button.state = State.Parent; 909 trackHover(true); 910 addChild(_text); 911 addChild(_button); 912 margins(Rect(2.pointsToPixels + 1, 0, 2.pointsToPixels + 1, 0)); 913 } 914 915 private bool onTextClick(Widget src) { 916 if (onPathSelectionListener.assigned) 917 return onPathSelectionListener(_path); 918 return false; 919 } 920 private bool onButtonClick(Widget src) { 921 // show popup menu with subdirs 922 string[] filters; 923 DirEntry[] entries; 924 try { 925 AttrFilter attrFilter = AttrFilter.dirs | AttrFilter.parent; 926 entries = listDirectory(_path, attrFilter); 927 } catch(Exception e) { 928 return false; 929 } 930 if (entries.length == 0) 931 return false; 932 MenuItem dirs = new MenuItem(); 933 int itemId = 25000; 934 foreach(ref DirEntry e; entries) { 935 string fullPath = e.name; 936 string d = baseName(fullPath); 937 Action a = new Action(itemId++, toUTF32(d)); 938 a.stringParam = fullPath; 939 MenuItem item = new MenuItem(a); 940 item.menuItemAction = delegate(const Action action) { 941 if (onPathSelectionListener.assigned) 942 return onPathSelectionListener(action.stringParam); 943 return false; 944 }; 945 dirs.add(item); 946 } 947 PopupMenu menuWidget = new PopupMenu(dirs); 948 PopupWidget popup = window.showPopup(menuWidget, this, PopupAlign.Below); 949 popup.flags = PopupFlags.CloseOnClickOutside; 950 return true; 951 } 952 } 953 954 /// Panel with buttons - path segments - for fast navigation to subdirs. 955 class FilePathPanelButtons : WidgetGroupDefaultDrawing { 956 protected string _path; 957 Listener!OnPathSelectionHandler onPathSelectionListener; 958 protected bool onPathSelected(string path) { 959 if (onPathSelectionListener.assigned) { 960 return onPathSelectionListener(path); 961 } 962 return false; 963 } 964 this(string ID = null) { 965 super(ID); 966 layoutWidth = FILL_PARENT; 967 clickable = true; 968 } 969 protected void initialize(string path) { 970 _path = path; 971 _children.clear(); 972 string itemPath = path; 973 for (;;) { 974 FilePathPanelItem item = new FilePathPanelItem(itemPath); 975 item.onPathSelectionListener = &onPathSelected; 976 addChild(item); 977 if (isRoot(itemPath)) { 978 break; 979 } 980 itemPath = parentDir(itemPath); 981 } 982 } 983 /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 984 override void measure(int parentWidth, int parentHeight) { 985 Rect m = margins; 986 Rect p = padding; 987 988 // calc size constraints for children 989 int pwidth = 0; 990 int pheight = 0; 991 if (parentWidth != SIZE_UNSPECIFIED) 992 pwidth = parentWidth - (m.left + m.right + p.left + p.right); 993 994 if (parentHeight != SIZE_UNSPECIFIED) 995 pheight = parentHeight - (m.top + m.bottom + p.top + p.bottom); 996 int reservedForEmptySpace = parentWidth / 20; 997 if (reservedForEmptySpace > 40.pointsToPixels) 998 reservedForEmptySpace = 40.pointsToPixels; 999 if (reservedForEmptySpace < 4.pointsToPixels) 1000 reservedForEmptySpace = 4.pointsToPixels; 1001 1002 Point sz; 1003 sz.x += reservedForEmptySpace; 1004 // measure children 1005 bool exceeded = false; 1006 for (int i = 0; i < _children.count; i++) { 1007 Widget item = _children.get(i); 1008 item.measure(pwidth, pheight); 1009 if (sz.y < item.measuredHeight) 1010 sz.y = item.measuredHeight; 1011 if (sz.x + item.measuredWidth > pwidth) { 1012 exceeded = true; 1013 } 1014 if (!exceeded || i == 0) // at least one item must be measured 1015 sz.x += item.measuredWidth; 1016 } 1017 measuredContent(parentWidth, parentHeight, sz.x, sz.y); 1018 } 1019 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 1020 override void layout(Rect rc) { 1021 //Log.d("tabControl.layout enter"); 1022 _needLayout = false; 1023 if (visibility == Visibility.Gone) { 1024 return; 1025 } 1026 _pos = rc; 1027 applyMargins(rc); 1028 applyPadding(rc); 1029 1030 int reservedForEmptySpace = rc.width / 20; 1031 if (reservedForEmptySpace > 40) 1032 reservedForEmptySpace = 40; 1033 if (reservedForEmptySpace < 4) 1034 reservedForEmptySpace = 4; 1035 int maxw = rc.width - reservedForEmptySpace; 1036 int totalw = 0; 1037 int visibleItems = 0; 1038 bool exceeded = false; 1039 // measure and update visibility 1040 for (int i = 0; i < _children.count; i++) { 1041 Widget item = _children.get(i); 1042 item.visibility = Visibility.Visible; 1043 item.measure(rc.width, rc.height); 1044 if (totalw + item.measuredWidth > rc.width) { 1045 exceeded = true; 1046 } 1047 if (!exceeded || i == 0) { // at least one item must be visible 1048 totalw += item.measuredWidth; 1049 visibleItems++; 1050 } else 1051 item.visibility = Visibility.Gone; 1052 } 1053 // layout visible items 1054 // backward order 1055 Rect itemRect = rc; 1056 for (int i = visibleItems - 1; i >= 0; i--) { 1057 Widget item = _children.get(i); 1058 int w = item.measuredWidth; 1059 if (i == visibleItems - 1 && w > maxw) 1060 w = maxw; 1061 itemRect.right = itemRect.left + w; 1062 item.layout(itemRect); 1063 itemRect.left += w; 1064 } 1065 1066 } 1067 1068 1069 } 1070 1071 interface PathSelectedHandler { 1072 bool onPathSelected(string path); 1073 } 1074 1075 /// Panel - either path segment buttons or text editor line 1076 class FilePathPanel : FrameLayout { 1077 Listener!OnPathSelectionHandler onPathSelectionListener; 1078 static const ID_SEGMENTS = "SEGMENTS"; 1079 static const ID_EDITOR = "ED_PATH"; 1080 protected FilePathPanelButtons _segments; 1081 protected EditLine _edPath; 1082 protected string _path; 1083 Signal!PathSelectedHandler pathListener; 1084 this(string ID = null) { 1085 super(ID); 1086 _segments = new FilePathPanelButtons(ID_SEGMENTS); 1087 _edPath = new EditLine(ID_EDITOR); 1088 _edPath.layoutWidth = FILL_PARENT; 1089 _edPath.enterKey = &onEnterKey; 1090 _edPath.focusChange = &onEditorFocusChanged; 1091 _segments.click = &onSegmentsClickOutside; 1092 _segments.onPathSelectionListener = &onPathSelected; 1093 addChild(_segments); 1094 addChild(_edPath); 1095 } 1096 1097 void setDefaultPopupMenu() { 1098 _edPath.setDefaultPopupMenu(); 1099 } 1100 1101 protected bool onEditorFocusChanged(Widget source, bool focused) { 1102 if (!focused) { 1103 _edPath.text = toUTF32(_path); 1104 showChild(ID_SEGMENTS); 1105 } 1106 return true; 1107 } 1108 protected bool onPathSelected(string path) { 1109 if (onPathSelectionListener.assigned) { 1110 if (exists(path)) 1111 return onPathSelectionListener(path); 1112 } 1113 return false; 1114 } 1115 protected bool onSegmentsClickOutside(Widget w) { 1116 // switch to editor 1117 _edPath.text = toUTF32(_path); 1118 showChild(ID_EDITOR); 1119 _edPath.setFocus(); 1120 return true; 1121 } 1122 protected bool onEnterKey(EditWidgetBase editor) { 1123 string fn = buildNormalizedPath(toUTF8(_edPath.text)); 1124 if (exists(fn) && isDir(fn)) 1125 onPathSelected(fn); 1126 return true; 1127 } 1128 1129 @property void path(string value) { 1130 _segments.initialize(value); 1131 _edPath.text = toUTF32(value); 1132 _path = value; 1133 showChild(ID_SEGMENTS); 1134 } 1135 @property string path() { 1136 return _path; 1137 } 1138 } 1139 1140 class FileNameEditLine : HorizontalLayout { 1141 protected EditLine _edFileName; 1142 protected Button _btn; 1143 protected string[string] _filetypeIcons; 1144 protected dstring _caption; 1145 protected uint _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist | FileDialogFlag.EnableCreateDirectory; 1146 protected FileFilterEntry[] _filters; 1147 protected int _filterIndex; 1148 1149 /// Modified state change listener (e.g. content has been saved, or first time modified after save) 1150 Signal!ModifiedStateListener modifiedStateChange; 1151 /// editor content is changed 1152 Signal!EditableContentChangeListener contentChange; 1153 1154 @property ref Signal!EditorActionHandler editorAction() { 1155 return _edFileName.editorAction; 1156 } 1157 1158 /// handle Enter key press inside line editor 1159 @property ref Signal!EnterKeyHandler enterKey() { 1160 return _edFileName.enterKey; 1161 } 1162 1163 void setDefaultPopupMenu() { 1164 _edFileName.setDefaultPopupMenu(); 1165 } 1166 1167 this(string ID = null) { 1168 super(ID); 1169 _caption = UIString.fromId("TITLE_OPEN_FILE"c).value; 1170 _edFileName = new EditLine("FileNameEditLine_edFileName"); 1171 _edFileName.minWidth(WIDGET_STYLE_CONSOLE ? 16 : 200); 1172 _edFileName.layoutWidth = FILL_PARENT; 1173 _btn = new Button("FileNameEditLine_btnFile", "..."d); 1174 _btn.styleId = STYLE_BUTTON_NOMARGINS; 1175 _btn.layoutWeight = 0; 1176 _btn.click = delegate(Widget src) { 1177 FileDialog dlg = new FileDialog(UIString.fromRaw(_caption), window, null, _fileDialogFlags); 1178 foreach(key, value; _filetypeIcons) 1179 dlg.filetypeIcons[key] = value; 1180 dlg.filters = _filters; 1181 dlg.dialogResult = delegate(Dialog dlg, const Action result) { 1182 if (result.id == ACTION_OPEN.id || result.id == ACTION_OPEN_DIRECTORY.id) { 1183 _edFileName.text = toUTF32(result.stringParam); 1184 if (contentChange.assigned) 1185 contentChange(_edFileName.content); 1186 } 1187 }; 1188 string path = toUTF8(_edFileName.text); 1189 if (!path.empty) { 1190 if (exists(path) && isFile(path)) { 1191 dlg.path = dirName(path); 1192 dlg.filename = baseName(path); 1193 } else if (exists(path) && isDir(path)) { 1194 dlg.path = path; 1195 } 1196 } 1197 dlg.show(); 1198 return true; 1199 }; 1200 _edFileName.contentChange = delegate(EditableContent content) { 1201 if (contentChange.assigned) 1202 contentChange(content); 1203 }; 1204 _edFileName.modifiedStateChange = delegate(Widget src, bool modified) { 1205 if (modifiedStateChange.assigned) 1206 modifiedStateChange(src, modified); 1207 }; 1208 addChild(_edFileName); 1209 addChild(_btn); 1210 } 1211 1212 @property uint fileDialogFlags() { return _fileDialogFlags; } 1213 @property void fileDialogFlags(uint f) { _fileDialogFlags = f; } 1214 1215 @property dstring caption() { return _caption; } 1216 @property void caption(dstring s) { _caption = s; } 1217 1218 /// returns widget content text (override to support this) 1219 override @property dstring text() const { return _edFileName.text; } 1220 /// sets widget content text (override to support this) 1221 override @property Widget text(dstring s) { _edFileName.text = s; return this; } 1222 /// sets widget content text (override to support this) 1223 override @property Widget text(UIString s) { _edFileName.text = s.value; return this; } 1224 1225 /// mapping of file extension to icon resource name, e.g. ".txt": "text-plain" 1226 @property ref string[string] filetypeIcons() { return _filetypeIcons; } 1227 1228 /// filter list for file type filter combo box 1229 @property FileFilterEntry[] filters() { 1230 return _filters; 1231 } 1232 1233 /// filter list for file type filter combo box 1234 @property void filters(FileFilterEntry[] values) { 1235 _filters = values; 1236 } 1237 1238 /// add new filter entry 1239 void addFilter(FileFilterEntry value) { 1240 _filters ~= value; 1241 } 1242 1243 /// filter index 1244 @property int filterIndex() { 1245 return _filterIndex; 1246 } 1247 1248 /// filter index 1249 @property void filterIndex(int index) { 1250 _filterIndex = index; 1251 } 1252 1253 @property bool readOnly() { return _edFileName.readOnly; } 1254 @property void readOnly(bool f) { _edFileName.readOnly = f; } 1255 1256 } 1257 1258 class DirEditLine : FileNameEditLine { 1259 this(string ID = null) { 1260 super(ID); 1261 _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable 1262 | FileDialogFlag.FileMustExist | FileDialogFlag.SelectDirectory | FileDialogFlag.EnableCreateDirectory; 1263 _caption = UIString.fromId("ACTION_SELECT_DIRECTORY"c); 1264 } 1265 } 1266 1267 //import dlangui.widgets.metadata; 1268 //mixin(registerWidgets!(FileNameEditLine, DirEditLine)());