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 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, " "d); 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 dstring appendSortOrderSuffix(dstring columnName, FileListSortOrder arrowUp, FileListSortOrder arrowDown) { 838 if (_sortOrder == arrowUp) 839 return columnName ~ " ▲"; 840 if (_sortOrder == arrowDown) 841 return columnName ~ " ▼"; 842 return columnName; 843 } 844 845 protected void updateColumnHeaders() { 846 _fileList.setColTitle(1, appendSortOrderSuffix(UIString.fromId("COL_NAME"c).value, FileListSortOrder.NAME_DESC, FileListSortOrder.NAME)); 847 _fileList.setColTitle(2, appendSortOrderSuffix(UIString.fromId("COL_SIZE"c).value, FileListSortOrder.SIZE_DESC, FileListSortOrder.SIZE)); 848 _fileList.setColTitle(3, appendSortOrderSuffix(UIString.fromId("COL_MODIFIED"c).value, FileListSortOrder.TIMESTAMP_DESC, FileListSortOrder.TIMESTAMP)); 849 } 850 851 protected void onHeaderCellClicked(GridWidgetBase source, int col, int row) { 852 debug Log.d("onHeaderCellClicked col=", col, " row=", row); 853 if (row == 0 && col >= 2 && col <= 4) { 854 // 2=NAME, 3=SIZE, 4=MODIFIED 855 changeSortOrder(col); 856 } 857 } 858 859 protected TextTypingShortcutHelper _shortcutHelper; 860 861 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 862 override void layout(Rect rc) { 863 super.layout(rc); 864 autofitGrid(); 865 } 866 867 868 ///// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 869 //override void measure(int parentWidth, int parentHeight) { 870 // super.measure(parentWidth, parentHeight); 871 // for(int i = 0; i < childCount; i++) { 872 // Widget w = child(i); 873 // Log.d("id=", w.id, " measuredHeight=", w.measuredHeight ); 874 // for (int j = 0; j < w.childCount; j++) { 875 // Widget w2 = w.child(j); 876 // Log.d(" id=", w2.id, " measuredHeight=", w.measuredHeight ); 877 // } 878 // } 879 // Log.d("this id=", id, " measuredHeight=", measuredHeight); 880 //} 881 882 override void onShow() { 883 _fileList.setFocus(); 884 } 885 } 886 887 interface OnPathSelectionHandler { 888 bool onPathSelected(string path); 889 } 890 891 class FilePathPanelItem : HorizontalLayout { 892 protected string _path; 893 protected TextWidget _text; 894 protected ImageButton _button; 895 Listener!OnPathSelectionHandler onPathSelectionListener; 896 this(string path) { 897 super(null); 898 styleId = STYLE_LIST_ITEM; 899 _path = path; 900 string fname = isRoot(path) ? path : baseName(path); 901 _text = new TextWidget(null, toUTF32(fname)); 902 _text.styleId = STYLE_BUTTON_TRANSPARENT; 903 _text.clickable = true; 904 _text.click = &onTextClick; 905 //_text.backgroundColor = 0xC0FFFF; 906 _text.state = State.Parent; 907 _button = new ImageButton(null, ATTR_SCROLLBAR_BUTTON_RIGHT); 908 _button.styleId = STYLE_BUTTON_TRANSPARENT; 909 _button.focusable = false; 910 _button.click = &onButtonClick; 911 //_button.backgroundColor = 0xC0FFC0; 912 _button.state = State.Parent; 913 trackHover(true); 914 addChild(_text); 915 addChild(_button); 916 margins(Rect(2.pointsToPixels + 1, 0, 2.pointsToPixels + 1, 0)); 917 } 918 919 private bool onTextClick(Widget src) { 920 if (onPathSelectionListener.assigned) 921 return onPathSelectionListener(_path); 922 return false; 923 } 924 private bool onButtonClick(Widget src) { 925 // show popup menu with subdirs 926 string[] filters; 927 DirEntry[] entries; 928 try { 929 AttrFilter attrFilter = AttrFilter.dirs | AttrFilter.parent; 930 entries = listDirectory(_path, attrFilter); 931 } catch(Exception e) { 932 return false; 933 } 934 if (entries.length == 0) 935 return false; 936 MenuItem dirs = new MenuItem(); 937 int itemId = 25000; 938 foreach(ref DirEntry e; entries) { 939 string fullPath = e.name; 940 string d = baseName(fullPath); 941 Action a = new Action(itemId++, toUTF32(d)); 942 a.stringParam = fullPath; 943 MenuItem item = new MenuItem(a); 944 item.menuItemAction = delegate(const Action action) { 945 if (onPathSelectionListener.assigned) 946 return onPathSelectionListener(action.stringParam); 947 return false; 948 }; 949 dirs.add(item); 950 } 951 PopupMenu menuWidget = new PopupMenu(dirs); 952 PopupWidget popup = window.showPopup(menuWidget, this, PopupAlign.Below); 953 popup.flags = PopupFlags.CloseOnClickOutside; 954 return true; 955 } 956 } 957 958 /// Panel with buttons - path segments - for fast navigation to subdirs. 959 class FilePathPanelButtons : WidgetGroupDefaultDrawing { 960 protected string _path; 961 Listener!OnPathSelectionHandler onPathSelectionListener; 962 protected bool onPathSelected(string path) { 963 if (onPathSelectionListener.assigned) { 964 return onPathSelectionListener(path); 965 } 966 return false; 967 } 968 this(string ID = null) { 969 super(ID); 970 layoutWidth = FILL_PARENT; 971 clickable = true; 972 } 973 protected void initialize(string path) { 974 _path = path; 975 _children.clear(); 976 string itemPath = path; 977 for (;;) { 978 FilePathPanelItem item = new FilePathPanelItem(itemPath); 979 item.onPathSelectionListener = &onPathSelected; 980 addChild(item); 981 if (isRoot(itemPath)) { 982 break; 983 } 984 itemPath = parentDir(itemPath); 985 } 986 } 987 /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 988 override void measure(int parentWidth, int parentHeight) { 989 Rect m = margins; 990 Rect p = padding; 991 992 // calc size constraints for children 993 int pwidth = 0; 994 int pheight = 0; 995 if (parentWidth != SIZE_UNSPECIFIED) 996 pwidth = parentWidth - (m.left + m.right + p.left + p.right); 997 998 if (parentHeight != SIZE_UNSPECIFIED) 999 pheight = parentHeight - (m.top + m.bottom + p.top + p.bottom); 1000 int reservedForEmptySpace = parentWidth / 20; 1001 if (reservedForEmptySpace > 40.pointsToPixels) 1002 reservedForEmptySpace = 40.pointsToPixels; 1003 if (reservedForEmptySpace < 4.pointsToPixels) 1004 reservedForEmptySpace = 4.pointsToPixels; 1005 1006 Point sz; 1007 sz.x += reservedForEmptySpace; 1008 // measure children 1009 bool exceeded = false; 1010 for (int i = 0; i < _children.count; i++) { 1011 Widget item = _children.get(i); 1012 item.measure(pwidth, pheight); 1013 if (sz.y < item.measuredHeight) 1014 sz.y = item.measuredHeight; 1015 if (sz.x + item.measuredWidth > pwidth) { 1016 exceeded = true; 1017 } 1018 if (!exceeded || i == 0) // at least one item must be measured 1019 sz.x += item.measuredWidth; 1020 } 1021 measuredContent(parentWidth, parentHeight, sz.x, sz.y); 1022 } 1023 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 1024 override void layout(Rect rc) { 1025 //Log.d("tabControl.layout enter"); 1026 _needLayout = false; 1027 if (visibility == Visibility.Gone) { 1028 return; 1029 } 1030 _pos = rc; 1031 applyMargins(rc); 1032 applyPadding(rc); 1033 1034 int reservedForEmptySpace = rc.width / 20; 1035 if (reservedForEmptySpace > 40) 1036 reservedForEmptySpace = 40; 1037 if (reservedForEmptySpace < 4) 1038 reservedForEmptySpace = 4; 1039 int maxw = rc.width - reservedForEmptySpace; 1040 int totalw = 0; 1041 int visibleItems = 0; 1042 bool exceeded = false; 1043 // measure and update visibility 1044 for (int i = 0; i < _children.count; i++) { 1045 Widget item = _children.get(i); 1046 item.visibility = Visibility.Visible; 1047 item.measure(rc.width, rc.height); 1048 if (totalw + item.measuredWidth > rc.width) { 1049 exceeded = true; 1050 } 1051 if (!exceeded || i == 0) { // at least one item must be visible 1052 totalw += item.measuredWidth; 1053 visibleItems++; 1054 } else 1055 item.visibility = Visibility.Gone; 1056 } 1057 // layout visible items 1058 // backward order 1059 Rect itemRect = rc; 1060 for (int i = visibleItems - 1; i >= 0; i--) { 1061 Widget item = _children.get(i); 1062 int w = item.measuredWidth; 1063 if (i == visibleItems - 1 && w > maxw) 1064 w = maxw; 1065 itemRect.right = itemRect.left + w; 1066 item.layout(itemRect); 1067 itemRect.left += w; 1068 } 1069 1070 } 1071 1072 1073 } 1074 1075 interface PathSelectedHandler { 1076 bool onPathSelected(string path); 1077 } 1078 1079 /// Panel - either path segment buttons or text editor line 1080 class FilePathPanel : FrameLayout { 1081 Listener!OnPathSelectionHandler onPathSelectionListener; 1082 static const ID_SEGMENTS = "SEGMENTS"; 1083 static const ID_EDITOR = "ED_PATH"; 1084 protected FilePathPanelButtons _segments; 1085 protected EditLine _edPath; 1086 protected string _path; 1087 Signal!PathSelectedHandler pathListener; 1088 this(string ID = null) { 1089 super(ID); 1090 _segments = new FilePathPanelButtons(ID_SEGMENTS); 1091 _edPath = new EditLine(ID_EDITOR); 1092 _edPath.layoutWidth = FILL_PARENT; 1093 _edPath.enterKey = &onEnterKey; 1094 _edPath.focusChange = &onEditorFocusChanged; 1095 _segments.click = &onSegmentsClickOutside; 1096 _segments.onPathSelectionListener = &onPathSelected; 1097 addChild(_segments); 1098 addChild(_edPath); 1099 } 1100 1101 void setDefaultPopupMenu() { 1102 _edPath.setDefaultPopupMenu(); 1103 } 1104 1105 protected bool onEditorFocusChanged(Widget source, bool focused) { 1106 if (!focused) { 1107 _edPath.text = toUTF32(_path); 1108 showChild(ID_SEGMENTS); 1109 } 1110 return true; 1111 } 1112 protected bool onPathSelected(string path) { 1113 if (onPathSelectionListener.assigned) { 1114 if (exists(path)) 1115 return onPathSelectionListener(path); 1116 } 1117 return false; 1118 } 1119 protected bool onSegmentsClickOutside(Widget w) { 1120 // switch to editor 1121 _edPath.text = toUTF32(_path); 1122 showChild(ID_EDITOR); 1123 _edPath.setFocus(); 1124 return true; 1125 } 1126 protected bool onEnterKey(EditWidgetBase editor) { 1127 string fn = buildNormalizedPath(toUTF8(_edPath.text)); 1128 if (exists(fn) && isDir(fn)) 1129 onPathSelected(fn); 1130 return true; 1131 } 1132 1133 @property void path(string value) { 1134 _segments.initialize(value); 1135 _edPath.text = toUTF32(value); 1136 _path = value; 1137 showChild(ID_SEGMENTS); 1138 } 1139 @property string path() { 1140 return _path; 1141 } 1142 } 1143 1144 class FileNameEditLine : HorizontalLayout { 1145 protected EditLine _edFileName; 1146 protected Button _btn; 1147 protected string[string] _filetypeIcons; 1148 protected dstring _caption; 1149 protected uint _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist | FileDialogFlag.EnableCreateDirectory; 1150 protected FileFilterEntry[] _filters; 1151 protected int _filterIndex; 1152 1153 /// Modified state change listener (e.g. content has been saved, or first time modified after save) 1154 Signal!ModifiedStateListener modifiedStateChange; 1155 /// editor content is changed 1156 Signal!EditableContentChangeListener contentChange; 1157 1158 @property ref Signal!EditorActionHandler editorAction() { 1159 return _edFileName.editorAction; 1160 } 1161 1162 /// handle Enter key press inside line editor 1163 @property ref Signal!EnterKeyHandler enterKey() { 1164 return _edFileName.enterKey; 1165 } 1166 1167 void setDefaultPopupMenu() { 1168 _edFileName.setDefaultPopupMenu(); 1169 } 1170 1171 this(string ID = null) { 1172 super(ID); 1173 _caption = UIString.fromId("TITLE_OPEN_FILE"c).value; 1174 _edFileName = new EditLine("FileNameEditLine_edFileName"); 1175 _edFileName.minWidth(WIDGET_STYLE_CONSOLE ? 16 : 200); 1176 _edFileName.layoutWidth = FILL_PARENT; 1177 _btn = new Button("FileNameEditLine_btnFile", "..."d); 1178 _btn.styleId = STYLE_BUTTON_NOMARGINS; 1179 _btn.layoutWeight = 0; 1180 _btn.click = delegate(Widget src) { 1181 FileDialog dlg = new FileDialog(UIString.fromRaw(_caption), window, null, _fileDialogFlags); 1182 foreach(key, value; _filetypeIcons) 1183 dlg.filetypeIcons[key] = value; 1184 dlg.filters = _filters; 1185 dlg.dialogResult = delegate(Dialog dlg, const Action result) { 1186 if (result.id == ACTION_OPEN.id || result.id == ACTION_OPEN_DIRECTORY.id) { 1187 _edFileName.text = toUTF32(result.stringParam); 1188 if (contentChange.assigned) 1189 contentChange(_edFileName.content); 1190 } 1191 }; 1192 string path = toUTF8(_edFileName.text); 1193 if (!path.empty) { 1194 if (exists(path) && isFile(path)) { 1195 dlg.path = dirName(path); 1196 dlg.filename = baseName(path); 1197 } else if (exists(path) && isDir(path)) { 1198 dlg.path = path; 1199 } 1200 } 1201 dlg.show(); 1202 return true; 1203 }; 1204 _edFileName.contentChange = delegate(EditableContent content) { 1205 if (contentChange.assigned) 1206 contentChange(content); 1207 }; 1208 _edFileName.modifiedStateChange = delegate(Widget src, bool modified) { 1209 if (modifiedStateChange.assigned) 1210 modifiedStateChange(src, modified); 1211 }; 1212 addChild(_edFileName); 1213 addChild(_btn); 1214 } 1215 1216 @property uint fileDialogFlags() { return _fileDialogFlags; } 1217 @property void fileDialogFlags(uint f) { _fileDialogFlags = f; } 1218 1219 @property dstring caption() { return _caption; } 1220 @property void caption(dstring s) { _caption = s; } 1221 1222 /// returns widget content text (override to support this) 1223 override @property dstring text() const { return _edFileName.text; } 1224 /// sets widget content text (override to support this) 1225 override @property Widget text(dstring s) { _edFileName.text = s; return this; } 1226 /// sets widget content text (override to support this) 1227 override @property Widget text(UIString s) { _edFileName.text = s.value; return this; } 1228 1229 /// mapping of file extension to icon resource name, e.g. ".txt": "text-plain" 1230 @property ref string[string] filetypeIcons() { return _filetypeIcons; } 1231 1232 /// filter list for file type filter combo box 1233 @property FileFilterEntry[] filters() { 1234 return _filters; 1235 } 1236 1237 /// filter list for file type filter combo box 1238 @property void filters(FileFilterEntry[] values) { 1239 _filters = values; 1240 } 1241 1242 /// add new filter entry 1243 void addFilter(FileFilterEntry value) { 1244 _filters ~= value; 1245 } 1246 1247 /// filter index 1248 @property int filterIndex() { 1249 return _filterIndex; 1250 } 1251 1252 /// filter index 1253 @property void filterIndex(int index) { 1254 _filterIndex = index; 1255 } 1256 1257 @property bool readOnly() { return _edFileName.readOnly; } 1258 @property void readOnly(bool f) { _edFileName.readOnly = f; } 1259 1260 } 1261 1262 class DirEditLine : FileNameEditLine { 1263 this(string ID = null) { 1264 super(ID); 1265 _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable 1266 | FileDialogFlag.FileMustExist | FileDialogFlag.SelectDirectory | FileDialogFlag.EnableCreateDirectory; 1267 _caption = UIString.fromId("ACTION_SELECT_DIRECTORY"c); 1268 } 1269 } 1270 1271 //import dlangui.widgets.metadata; 1272 //mixin(registerWidgets!(FileNameEditLine, DirEditLine)());