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