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