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 (BACKEND_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 = BACKEND_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 (BACKEND_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 lisk 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 _roots = getRootPaths() ~ getBookmarkPaths(); 717 718 layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).minWidth(BACKEND_CONSOLE ? 50 : 600); 719 //minHeight = 400; 720 721 LinearLayout content = new HorizontalLayout("dlgcontent"); 722 723 content.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); //.minWidth(400).minHeight(300); 724 725 726 //leftPanel = new VerticalLayout("places"); 727 //leftPanel.addChild(createRootsList()); 728 //leftPanel.layoutHeight(FILL_PARENT).minWidth(BACKEND_CONSOLE ? 7 : 40); 729 730 leftPanel = createRootsList(); 731 leftPanel.minWidth(BACKEND_CONSOLE ? 7 : 40.pointsToPixels); 732 733 rightPanel = new VerticalLayout("main"); 734 rightPanel.layoutHeight(FILL_PARENT).layoutWidth(FILL_PARENT); 735 rightPanel.addChild(new TextWidget(null, UIString.fromId("MESSAGE_PATH"c) ~ ":")); 736 737 content.addChild(leftPanel); 738 content.addChild(rightPanel); 739 740 _edPath = new FilePathPanel("path"); 741 _edPath.layoutWidth(FILL_PARENT); 742 _edPath.layoutWeight = 0; 743 _edPath.onPathSelectionListener = &onPathSelected; 744 HorizontalLayout fnlayout = new HorizontalLayout(); 745 fnlayout.layoutWidth(FILL_PARENT); 746 _edFilename = new EditLine("filename"); 747 _edFilename.layoutWidth(FILL_PARENT); 748 if (_flags & FileDialogFlag.SelectDirectory) { 749 _edFilename.visibility = Visibility.Gone; 750 } 751 752 //_edFilename.layoutWeight = 0; 753 fnlayout.addChild(_edFilename); 754 if (_filters.length) { 755 dstring[] filterLabels; 756 foreach(f; _filters) 757 filterLabels ~= f.label.value; 758 _cbFilters = new ComboBox("filter", filterLabels); 759 _cbFilters.selectedItemIndex = _filterIndex; 760 _cbFilters.itemClick = delegate(Widget source, int itemIndex) { 761 _filterIndex = itemIndex; 762 reopenDirectory(); 763 return true; 764 }; 765 _cbFilters.layoutWidth(WRAP_CONTENT); 766 _cbFilters.layoutWeight(0); 767 //_cbFilters.backgroundColor = 0xFFC0FF; 768 fnlayout.addChild(_cbFilters); 769 //fnlayout.backgroundColor = 0xFFFFC0; 770 } 771 772 _fileList = new StringGridWidget("files"); 773 _fileList.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); 774 _fileList.fullColumnOnLeft(false); 775 _fileList.fullRowOnTop(false); 776 _fileList.resize(4, 3); 777 _fileList.setColTitle(0, " "d); 778 updateColumnHeaders(); 779 _fileList.showRowHeaders = false; 780 _fileList.rowSelect = true; 781 _fileList.multiSelect = _allowMultipleFiles; 782 _fileList.cellPopupMenu = &getCellPopupMenu; 783 _fileList.menuItemAction = &handleAction; 784 _fileList.minVisibleRows = 10; 785 _fileList.minVisibleCols = 4; 786 _fileList.headerCellClicked = &onHeaderCellClicked; 787 788 _fileList.keyEvent = delegate(Widget source, KeyEvent event) { 789 if (_shortcutHelper.onKeyEvent(event)) 790 locateFileInList(_shortcutHelper.text); 791 return false; 792 }; 793 794 rightPanel.addChild(_edPath); 795 rightPanel.addChild(_fileList); 796 rightPanel.addChild(fnlayout); 797 798 799 addChild(content); 800 if (_flags & FileDialogFlag.EnableCreateDirectory) { 801 addChild(createButtonsPanel([ACTION_CREATE_DIRECTORY, cast(immutable)_action, ACTION_CANCEL], 1, 1)); 802 } else { 803 addChild(createButtonsPanel([cast(immutable)_action, ACTION_CANCEL], 0, 0)); 804 } 805 806 _fileList.customCellAdapter = this; 807 _fileList.cellActivated = delegate(GridWidgetBase source, int col, int row) { 808 onItemActivated(row); 809 }; 810 _fileList.cellSelected = delegate(GridWidgetBase source, int col, int row) { 811 onItemSelected(row); 812 }; 813 814 if (_path.empty || !_path.exists || !_path.isDir) { 815 _path = currentDir; 816 if (!_path.exists || !_path.isDir) 817 _path = homePath; 818 } 819 openDirectory(_path, _filename); 820 _fileList.layoutHeight = FILL_PARENT; 821 822 } 823 824 /// get sort order suffix for column title 825 protected dstring appendSortOrderSuffix(dstring columnName, FileListSortOrder arrowUp, FileListSortOrder arrowDown) { 826 if (_sortOrder == arrowUp) 827 return columnName ~ " ▲"; 828 if (_sortOrder == arrowDown) 829 return columnName ~ " ▼"; 830 return columnName; 831 } 832 833 protected void updateColumnHeaders() { 834 _fileList.setColTitle(1, appendSortOrderSuffix(UIString.fromId("COL_NAME"c).value, FileListSortOrder.NAME_DESC, FileListSortOrder.NAME)); 835 _fileList.setColTitle(2, appendSortOrderSuffix(UIString.fromId("COL_SIZE"c).value, FileListSortOrder.SIZE_DESC, FileListSortOrder.SIZE)); 836 _fileList.setColTitle(3, appendSortOrderSuffix(UIString.fromId("COL_MODIFIED"c).value, FileListSortOrder.TIMESTAMP_DESC, FileListSortOrder.TIMESTAMP)); 837 } 838 839 protected void onHeaderCellClicked(GridWidgetBase source, int col, int row) { 840 debug Log.d("onHeaderCellClicked col=", col, " row=", row); 841 if (row == 0 && col >= 2 && col <= 4) { 842 // 2=NAME, 3=SIZE, 4=MODIFIED 843 changeSortOrder(col); 844 } 845 } 846 847 protected TextTypingShortcutHelper _shortcutHelper; 848 849 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 850 override void layout(Rect rc) { 851 super.layout(rc); 852 autofitGrid(); 853 } 854 855 856 ///// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 857 //override void measure(int parentWidth, int parentHeight) { 858 // super.measure(parentWidth, parentHeight); 859 // for(int i = 0; i < childCount; i++) { 860 // Widget w = child(i); 861 // Log.d("id=", w.id, " measuredHeight=", w.measuredHeight ); 862 // for (int j = 0; j < w.childCount; j++) { 863 // Widget w2 = w.child(j); 864 // Log.d(" id=", w2.id, " measuredHeight=", w.measuredHeight ); 865 // } 866 // } 867 // Log.d("this id=", id, " measuredHeight=", measuredHeight); 868 //} 869 870 override void onShow() { 871 _fileList.setFocus(); 872 } 873 } 874 875 interface OnPathSelectionHandler { 876 bool onPathSelected(string path); 877 } 878 879 class FilePathPanelItem : HorizontalLayout { 880 protected string _path; 881 protected TextWidget _text; 882 protected ImageButton _button; 883 Listener!OnPathSelectionHandler onPathSelectionListener; 884 this(string path) { 885 super(null); 886 styleId = STYLE_LIST_ITEM; 887 _path = path; 888 string fname = isRoot(path) ? path : baseName(path); 889 _text = new TextWidget(null, toUTF32(fname)); 890 _text.styleId = STYLE_BUTTON_TRANSPARENT; 891 _text.clickable = true; 892 _text.click = &onTextClick; 893 //_text.backgroundColor = 0xC0FFFF; 894 _text.state = State.Parent; 895 _button = new ImageButton(null, ATTR_SCROLLBAR_BUTTON_RIGHT); 896 _button.styleId = STYLE_BUTTON_TRANSPARENT; 897 _button.focusable = false; 898 _button.click = &onButtonClick; 899 //_button.backgroundColor = 0xC0FFC0; 900 _button.state = State.Parent; 901 trackHover(true); 902 addChild(_text); 903 addChild(_button); 904 margins(Rect(2.pointsToPixels + 1, 0, 2.pointsToPixels + 1, 0)); 905 } 906 907 private bool onTextClick(Widget src) { 908 if (onPathSelectionListener.assigned) 909 return onPathSelectionListener(_path); 910 return false; 911 } 912 private bool onButtonClick(Widget src) { 913 // show popup menu with subdirs 914 string[] filters; 915 DirEntry[] entries; 916 try { 917 AttrFilter attrFilter = AttrFilter.dirs | AttrFilter.parent; 918 entries = listDirectory(_path, attrFilter); 919 } catch(Exception e) { 920 return false; 921 } 922 if (entries.length == 0) 923 return false; 924 MenuItem dirs = new MenuItem(); 925 int itemId = 25000; 926 foreach(ref DirEntry e; entries) { 927 string fullPath = e.name; 928 string d = baseName(fullPath); 929 Action a = new Action(itemId++, toUTF32(d)); 930 a.stringParam = fullPath; 931 MenuItem item = new MenuItem(a); 932 item.menuItemAction = delegate(const Action action) { 933 if (onPathSelectionListener.assigned) 934 return onPathSelectionListener(action.stringParam); 935 return false; 936 }; 937 dirs.add(item); 938 } 939 PopupMenu menuWidget = new PopupMenu(dirs); 940 PopupWidget popup = window.showPopup(menuWidget, this, PopupAlign.Below); 941 popup.flags = PopupFlags.CloseOnClickOutside; 942 return true; 943 } 944 } 945 946 /// Panel with buttons - path segments - for fast navigation to subdirs. 947 class FilePathPanelButtons : WidgetGroupDefaultDrawing { 948 protected string _path; 949 Listener!OnPathSelectionHandler onPathSelectionListener; 950 protected bool onPathSelected(string path) { 951 if (onPathSelectionListener.assigned) { 952 return onPathSelectionListener(path); 953 } 954 return false; 955 } 956 this(string ID = null) { 957 super(ID); 958 layoutWidth = FILL_PARENT; 959 clickable = true; 960 } 961 protected void initialize(string path) { 962 _path = path; 963 _children.clear(); 964 string itemPath = path; 965 for (;;) { 966 FilePathPanelItem item = new FilePathPanelItem(itemPath); 967 item.onPathSelectionListener = &onPathSelected; 968 addChild(item); 969 if (isRoot(itemPath)) { 970 break; 971 } 972 itemPath = parentDir(itemPath); 973 } 974 } 975 /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 976 override void measure(int parentWidth, int parentHeight) { 977 Rect m = margins; 978 Rect p = padding; 979 980 // calc size constraints for children 981 int pwidth = 0; 982 int pheight = 0; 983 if (parentWidth != SIZE_UNSPECIFIED) 984 pwidth = parentWidth - (m.left + m.right + p.left + p.right); 985 986 if (parentHeight != SIZE_UNSPECIFIED) 987 pheight = parentHeight - (m.top + m.bottom + p.top + p.bottom); 988 int reservedForEmptySpace = parentWidth / 20; 989 if (reservedForEmptySpace > 40.pointsToPixels) 990 reservedForEmptySpace = 40.pointsToPixels; 991 if (reservedForEmptySpace < 4.pointsToPixels) 992 reservedForEmptySpace = 4.pointsToPixels; 993 994 Point sz; 995 sz.x += reservedForEmptySpace; 996 // measure children 997 bool exceeded = false; 998 for (int i = 0; i < _children.count; i++) { 999 Widget item = _children.get(i); 1000 item.measure(pwidth, pheight); 1001 if (sz.y < item.measuredHeight) 1002 sz.y = item.measuredHeight; 1003 if (sz.x + item.measuredWidth > pwidth) { 1004 exceeded = true; 1005 } 1006 if (!exceeded || i == 0) // at least one item must be measured 1007 sz.x += item.measuredWidth; 1008 } 1009 measuredContent(parentWidth, parentHeight, sz.x, sz.y); 1010 } 1011 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 1012 override void layout(Rect rc) { 1013 //Log.d("tabControl.layout enter"); 1014 _needLayout = false; 1015 if (visibility == Visibility.Gone) { 1016 return; 1017 } 1018 _pos = rc; 1019 applyMargins(rc); 1020 applyPadding(rc); 1021 1022 int reservedForEmptySpace = rc.width / 20; 1023 if (reservedForEmptySpace > 40) 1024 reservedForEmptySpace = 40; 1025 if (reservedForEmptySpace < 4) 1026 reservedForEmptySpace = 4; 1027 int maxw = rc.width - reservedForEmptySpace; 1028 int totalw = 0; 1029 int visibleItems = 0; 1030 bool exceeded = false; 1031 // measure and update visibility 1032 for (int i = 0; i < _children.count; i++) { 1033 Widget item = _children.get(i); 1034 item.visibility = Visibility.Visible; 1035 item.measure(rc.width, rc.height); 1036 if (totalw + item.measuredWidth > rc.width) { 1037 exceeded = true; 1038 } 1039 if (!exceeded || i == 0) { // at least one item must be visible 1040 totalw += item.measuredWidth; 1041 visibleItems++; 1042 } else 1043 item.visibility = Visibility.Gone; 1044 } 1045 // layout visible items 1046 // backward order 1047 Rect itemRect = rc; 1048 for (int i = visibleItems - 1; i >= 0; i--) { 1049 Widget item = _children.get(i); 1050 int w = item.measuredWidth; 1051 if (i == visibleItems - 1 && w > maxw) 1052 w = maxw; 1053 itemRect.right = itemRect.left + w; 1054 item.layout(itemRect); 1055 itemRect.left += w; 1056 } 1057 1058 } 1059 1060 1061 } 1062 1063 interface PathSelectedHandler { 1064 bool onPathSelected(string path); 1065 } 1066 1067 /// Panel - either path segment buttons or text editor line 1068 class FilePathPanel : FrameLayout { 1069 Listener!OnPathSelectionHandler onPathSelectionListener; 1070 static const ID_SEGMENTS = "SEGMENTS"; 1071 static const ID_EDITOR = "ED_PATH"; 1072 protected FilePathPanelButtons _segments; 1073 protected EditLine _edPath; 1074 protected string _path; 1075 Signal!PathSelectedHandler pathListener; 1076 this(string ID = null) { 1077 super(ID); 1078 _segments = new FilePathPanelButtons(ID_SEGMENTS); 1079 _edPath = new EditLine(ID_EDITOR); 1080 _edPath.layoutWidth = FILL_PARENT; 1081 _edPath.enterKey = &onEnterKey; 1082 _edPath.focusChange = &onEditorFocusChanged; 1083 _segments.click = &onSegmentsClickOutside; 1084 _segments.onPathSelectionListener = &onPathSelected; 1085 addChild(_segments); 1086 addChild(_edPath); 1087 } 1088 protected bool onEditorFocusChanged(Widget source, bool focused) { 1089 if (!focused) { 1090 _edPath.text = toUTF32(_path); 1091 showChild(ID_SEGMENTS); 1092 } 1093 return true; 1094 } 1095 protected bool onPathSelected(string path) { 1096 if (onPathSelectionListener.assigned) { 1097 if (exists(path)) 1098 return onPathSelectionListener(path); 1099 } 1100 return false; 1101 } 1102 protected bool onSegmentsClickOutside(Widget w) { 1103 // switch to editor 1104 _edPath.text = toUTF32(_path); 1105 showChild(ID_EDITOR); 1106 _edPath.setFocus(); 1107 return true; 1108 } 1109 protected bool onEnterKey(EditWidgetBase editor) { 1110 string fn = buildNormalizedPath(toUTF8(_edPath.text)); 1111 if (exists(fn) && isDir(fn)) 1112 onPathSelected(fn); 1113 return true; 1114 } 1115 1116 @property void path(string value) { 1117 _segments.initialize(value); 1118 _edPath.text = toUTF32(value); 1119 _path = value; 1120 showChild(ID_SEGMENTS); 1121 } 1122 @property string path() { 1123 return _path; 1124 } 1125 } 1126 1127 class FileNameEditLine : HorizontalLayout { 1128 protected EditLine _edFileName; 1129 protected Button _btn; 1130 protected string[string] _filetypeIcons; 1131 protected dstring _caption; 1132 protected uint _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist | FileDialogFlag.EnableCreateDirectory; 1133 protected FileFilterEntry[] _filters; 1134 protected int _filterIndex; 1135 1136 /// Modified state change listener (e.g. content has been saved, or first time modified after save) 1137 Signal!ModifiedStateListener modifiedStateChange; 1138 /// editor content is changed 1139 Signal!EditableContentChangeListener contentChange; 1140 1141 @property ref Signal!EditorActionHandler editorAction() { 1142 return _edFileName.editorAction; 1143 } 1144 1145 /// handle Enter key press inside line editor 1146 @property ref Signal!EnterKeyHandler enterKey() { 1147 return _edFileName.enterKey; 1148 } 1149 1150 this(string ID = null) { 1151 super(ID); 1152 _caption = UIString.fromId("TITLE_OPEN_FILE"c).value; 1153 _edFileName = new EditLine("FileNameEditLine_edFileName"); 1154 _edFileName.minWidth(BACKEND_CONSOLE ? 16 : 200); 1155 _edFileName.layoutWidth = FILL_PARENT; 1156 _btn = new Button("FileNameEditLine_btnFile", "..."d); 1157 _btn.styleId = STYLE_BUTTON_NOMARGINS; 1158 _btn.layoutWeight = 0; 1159 _btn.click = delegate(Widget src) { 1160 FileDialog dlg = new FileDialog(UIString.fromRaw(_caption), window, null, _fileDialogFlags); 1161 foreach(key, value; _filetypeIcons) 1162 dlg.filetypeIcons[key] = value; 1163 dlg.filters = _filters; 1164 dlg.dialogResult = delegate(Dialog dlg, const Action result) { 1165 if (result.id == ACTION_OPEN.id || result.id == ACTION_OPEN_DIRECTORY.id) { 1166 _edFileName.text = toUTF32(result.stringParam); 1167 if (contentChange.assigned) 1168 contentChange(_edFileName.content); 1169 } 1170 }; 1171 string path = toUTF8(_edFileName.text); 1172 if (!path.empty) { 1173 if (exists(path) && isFile(path)) { 1174 dlg.path = dirName(path); 1175 dlg.filename = baseName(path); 1176 } else if (exists(path) && isDir(path)) { 1177 dlg.path = path; 1178 } 1179 } 1180 dlg.show(); 1181 return true; 1182 }; 1183 _edFileName.contentChange = delegate(EditableContent content) { 1184 if (contentChange.assigned) 1185 contentChange(content); 1186 }; 1187 _edFileName.modifiedStateChange = delegate(Widget src, bool modified) { 1188 if (modifiedStateChange.assigned) 1189 modifiedStateChange(src, modified); 1190 }; 1191 addChild(_edFileName); 1192 addChild(_btn); 1193 } 1194 1195 @property uint fileDialogFlags() { return _fileDialogFlags; } 1196 @property void fileDialogFlags(uint f) { _fileDialogFlags = f; } 1197 1198 @property dstring caption() { return _caption; } 1199 @property void caption(dstring s) { _caption = s; } 1200 1201 /// returns widget content text (override to support this) 1202 override @property dstring text() { return _edFileName.text; } 1203 /// sets widget content text (override to support this) 1204 override @property Widget text(dstring s) { _edFileName.text = s; return this; } 1205 /// sets widget content text (override to support this) 1206 override @property Widget text(UIString s) { _edFileName.text = s.value; return this; } 1207 1208 /// mapping of file extension to icon resource name, e.g. ".txt": "text-plain" 1209 @property ref string[string] filetypeIcons() { return _filetypeIcons; } 1210 1211 /// filter list for file type filter combo box 1212 @property FileFilterEntry[] filters() { 1213 return _filters; 1214 } 1215 1216 /// filter list for file type filter combo box 1217 @property void filters(FileFilterEntry[] values) { 1218 _filters = values; 1219 } 1220 1221 /// add new filter entry 1222 void addFilter(FileFilterEntry value) { 1223 _filters ~= value; 1224 } 1225 1226 /// filter index 1227 @property int filterIndex() { 1228 return _filterIndex; 1229 } 1230 1231 /// filter index 1232 @property void filterIndex(int index) { 1233 _filterIndex = index; 1234 } 1235 1236 @property bool readOnly() { return _edFileName.readOnly; } 1237 @property void readOnly(bool f) { _edFileName.readOnly = f; } 1238 1239 } 1240 1241 class DirEditLine : FileNameEditLine { 1242 this(string ID = null) { 1243 super(ID); 1244 _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable 1245 | FileDialogFlag.FileMustExist | FileDialogFlag.SelectDirectory | FileDialogFlag.EnableCreateDirectory; 1246 _caption = UIString.fromId("ACTION_SELECT_DIRECTORY"c); 1247 } 1248 } 1249 1250 //import dlangui.widgets.metadata; 1251 //mixin(registerWidgets!(FileNameEditLine, DirEditLine)());