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 private import std.array : split; 49 50 51 /// flags for file dialog options 52 enum FileDialogFlag : uint { 53 /// file must exist (use this for open dialog) 54 FileMustExist = 0x100, 55 /// ask before saving to existing 56 ConfirmOverwrite = 0x200, 57 /// select directory, not file 58 SelectDirectory = 0x400, 59 /// show Create Directory button 60 EnableCreateDirectory = 0x800, 61 /// flags for Open dialog 62 Open = FileMustExist | EnableCreateDirectory, 63 /// flags for Save dialog 64 Save = ConfirmOverwrite | EnableCreateDirectory, 65 66 } 67 68 /// File dialog action codes 69 enum FileDialogActions : int { 70 ShowInFileManager = 4000, 71 CreateDirectory = 4001, 72 DeleteFile = 4002, 73 } 74 75 /// filetype filter entry for FileDialog 76 struct FileFilterEntry { 77 UIString label; 78 string[] filter; 79 bool executableOnly; 80 this(UIString displayLabel, string filterList, bool executableOnly = false) { 81 label = displayLabel; 82 if (filterList.length) 83 filter = split(filterList, ";"); 84 this.executableOnly = executableOnly; 85 } 86 } 87 88 89 /// File open / save dialog 90 class FileDialog : Dialog, CustomGridCellAdapter { 91 protected FilePathPanel _edPath; 92 protected EditLine _edFilename; 93 protected ComboBox _cbFilters; 94 protected StringGridWidget _fileList; 95 protected Widget leftPanel; 96 protected VerticalLayout rightPanel; 97 protected Action _action; 98 99 protected RootEntry[] _roots; 100 protected FileFilterEntry[] _filters; 101 protected int _filterIndex; 102 protected string _path; 103 protected string _filename; 104 protected DirEntry[] _entries; 105 protected bool _isRoot; 106 107 protected bool _isOpenDialog; 108 109 protected bool _showHiddenFiles; 110 protected bool _allowMultipleFiles; 111 112 protected string[string] _filetypeIcons; 113 114 this(UIString caption, Window parent, Action action = null, uint fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist) { 115 super(caption, parent, fileDialogFlags | (Platform.instance.uiDialogDisplayMode & DialogDisplayMode.fileDialogInPopup ? DialogFlag.Popup : 0)); 116 _isOpenDialog = !(_flags & FileDialogFlag.ConfirmOverwrite); 117 if (action is null) { 118 if (fileDialogFlags & FileDialogFlag.SelectDirectory) 119 action = ACTION_OPEN_DIRECTORY.clone(); 120 else if (_isOpenDialog) 121 action = ACTION_OPEN.clone(); 122 else 123 action = ACTION_SAVE.clone(); 124 } 125 _action = action; 126 } 127 128 /// mapping of file extension to icon resource name, e.g. ".txt": "text-plain" 129 @property ref string[string] filetypeIcons() { return _filetypeIcons; } 130 131 /// filter list for file type filter combo box 132 @property FileFilterEntry[] filters() { 133 return _filters; 134 } 135 136 /// filter list for file type filter combo box 137 @property void filters(FileFilterEntry[] values) { 138 _filters = values; 139 } 140 141 /// add new filter entry 142 void addFilter(FileFilterEntry value) { 143 _filters ~= value; 144 } 145 146 /// filter index 147 @property int filterIndex() { 148 return _filterIndex; 149 } 150 151 /// filter index 152 @property void filterIndex(int index) { 153 _filterIndex = index; 154 } 155 156 /// the path to the directory whose files should be displayed 157 @property string path() { 158 return _path; 159 } 160 161 @property void path(string s) { 162 _path = s; 163 } 164 165 /// the name of the file or directory that is currently selected 166 @property string filename() { 167 return _filename; 168 } 169 170 @property void filename(string s) { 171 _filename = s; 172 } 173 174 /// all the selected filenames 175 @property string[] filenames() { 176 string[] res; 177 res.reserve(_fileList.selection.length); 178 int i = 0; 179 foreach (val; _fileList.selection) { 180 res ~= _entries[val.y]; 181 ++i; 182 } 183 return res; 184 } 185 186 @property bool showHiddenFiles() { 187 return _showHiddenFiles; 188 } 189 190 @property void showHiddenFiles(bool b) { 191 _showHiddenFiles = b; 192 } 193 194 @property bool allowMultipleFiles() { 195 return _allowMultipleFiles; 196 } 197 198 @property void allowMultipleFiles(bool b) { 199 _allowMultipleFiles = b; 200 } 201 202 /// return currently selected filter value - array of patterns like ["*.txt", "*.rtf"] 203 @property string[] selectedFilter() { 204 if (_filterIndex >= 0 && _filterIndex < _filters.length) 205 return _filters[_filterIndex].filter; 206 return null; 207 } 208 209 @property bool executableFilterSelected() { 210 if (_filterIndex >= 0 && _filterIndex < _filters.length) 211 return _filters[_filterIndex].executableOnly; 212 return false; 213 } 214 215 protected bool upLevel() { 216 return openDirectory(parentDir(_path), _path); 217 } 218 219 protected bool reopenDirectory() { 220 return openDirectory(_path, null); 221 } 222 223 protected void locateFileInList(dstring pattern) { 224 if (!pattern.length) 225 return; 226 int selection = _fileList.row; 227 if (selection < 0) 228 selection = 0; 229 int index = -1; // first matched item 230 string mask = pattern.toUTF8; 231 // search forward from current row to end of list 232 for(int i = selection; i < _entries.length; i++) { 233 string fname = baseName(_entries[i].name); 234 if (fname.startsWith(mask)) { 235 index = i; 236 break; 237 } 238 } 239 if (index < 0) { 240 // search from beginning of list to current position 241 for(int i = 0; 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 } 249 if (index >= 0) { 250 // move selection 251 _fileList.selectCell(1, index + 1); 252 window.update(); 253 } 254 } 255 256 protected bool openDirectory(string dir, string selectedItemPath) { 257 dir = buildNormalizedPath(dir); 258 Log.d("FileDialog.openDirectory(", dir, ")"); 259 DirEntry[] entries; 260 261 auto attrFilter = (showHiddenFiles ? AttrFilter.all : AttrFilter.allVisible) | AttrFilter.special | AttrFilter.parent; 262 if (executableFilterSelected()) { 263 attrFilter |= AttrFilter.executable; 264 } 265 try { 266 _entries = listDirectory(dir, attrFilter, selectedFilter()); 267 } catch(Exception e) { 268 import dlangui.dialogs.msgbox; 269 auto msgBox = new MessageBox(UIString.fromRaw("Error"d), UIString.fromRaw(e.msg.toUTF32), window()); 270 msgBox.show(); 271 return false; 272 } 273 _fileList.rows = 0; 274 _path = dir; 275 _isRoot = isRoot(dir); 276 _edPath.path = _path; //toUTF32(_path); 277 _fileList.rows = cast(int)_entries.length; 278 int selectionIndex = -1; 279 for (int i = 0; i < _entries.length; i++) { 280 if (_entries[i].name.equal(selectedItemPath)) 281 selectionIndex = i; 282 string fname = baseName(_entries[i].name); 283 string sz; 284 string date; 285 bool d = _entries[i].isDir; 286 _fileList.setCellText(1, i, toUTF32(fname)); 287 if (d) { 288 _fileList.setCellText(0, i, "folder"); 289 } else { 290 string ext = extension(fname); 291 string resname; 292 if (ext in _filetypeIcons) 293 resname = _filetypeIcons[ext]; 294 else if (baseName(fname) in _filetypeIcons) 295 resname = _filetypeIcons[baseName(fname)]; 296 else 297 resname = "text-plain"; 298 _fileList.setCellText(0, i, toUTF32(resname)); 299 double size = _entries[i].size; 300 import std.format : format; 301 sz = size < 1024 ? to!string(size) ~ " B" : 302 (size < 1024*1024 ? "%.1f".format(size/1024) ~ " KB" : 303 (size < 1024*1024*1024 ? "%.1f".format(size/(1024*1024)) ~ " MB" : 304 "%.1f".format(size/(1024*1024*1024)) ~ " GB")); 305 import std.datetime; 306 SysTime ts = _entries[i].timeLastModified; 307 //string timeString = "%04d.%02d.%02d %02d:%02d:%02d".format(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second); 308 string timeString = "%04d.%02d.%02d %02d:%02d".format(ts.year, ts.month, ts.day, ts.hour, ts.minute); 309 date = timeString; 310 } 311 _fileList.setCellText(2, i, toUTF32(sz)); 312 _fileList.setCellText(3, i, toUTF32(date)); 313 } 314 if(_fileList.height > 0) 315 _fileList.scrollTo(0, 0); 316 317 autofitGrid(); 318 if (selectionIndex >= 0) 319 _fileList.selectCell(1, selectionIndex + 1, true); 320 else if (_entries.length > 0) 321 _fileList.selectCell(1, 1, true); 322 return true; 323 } 324 325 void autofitGrid() { 326 _fileList.autoFitColumnWidths(); 327 //_fileList.setColWidth(1, 0); 328 _fileList.fillColumnWidth(1); 329 } 330 331 override bool onKeyEvent(KeyEvent event) { 332 if (event.action == KeyAction.KeyDown) { 333 if (event.keyCode == KeyCode.BACK && event.flags == 0) { 334 upLevel(); 335 return true; 336 } 337 } 338 return super.onKeyEvent(event); 339 } 340 341 /// return true for custom drawn cell 342 override bool isCustomCell(int col, int row) { 343 if ((col == 0 || col == 1) && row >= 0) 344 return true; 345 return false; 346 } 347 348 protected DrawableRef rowIcon(int row) { 349 string iconId = toUTF8(_fileList.cellText(0, row)); 350 DrawableRef res; 351 if (iconId.length) 352 res = drawableCache.get(iconId); 353 return res; 354 } 355 356 /// return cell size 357 override Point measureCell(int col, int row) { 358 if (col == 1) { 359 FontRef fnt = _fileList.font; 360 dstring txt = _fileList.cellText(col, row); 361 Point sz = fnt.textSize(txt); 362 if (sz.y < fnt.height) 363 sz.y = fnt.height; 364 return sz; 365 } 366 if (BACKEND_CONSOLE) 367 return Point(0, 0); 368 DrawableRef icon = rowIcon(row); 369 if (icon.isNull) 370 return Point(0, 0); 371 return Point(icon.width + 2.pointsToPixels, icon.height + 2.pointsToPixels); 372 } 373 374 /// draw data cell content 375 override void drawCell(DrawBuf buf, Rect rc, int col, int row) { 376 if (col == 1) { 377 if (BACKEND_GUI) 378 rc.shrink(2, 1); 379 else 380 rc.right--; 381 FontRef fnt = _fileList.font; 382 dstring txt = _fileList.cellText(col, row); 383 Point sz = fnt.textSize(txt); 384 Align ha = Align.Left; 385 //if (sz.y < rc.height) 386 // applyAlign(rc, sz, ha, Align.VCenter); 387 int offset = BACKEND_CONSOLE ? 0 : 1; 388 uint cl = _fileList.textColor; 389 if (_entries[row].isDir) 390 cl = style.customColor("file_dialog_dir_name_color", cl); 391 fnt.drawText(buf, rc.left + offset, rc.top + offset, txt, cl); 392 return; 393 } 394 DrawableRef img = rowIcon(row); 395 if (!img.isNull) { 396 Point sz; 397 sz.x = img.width; 398 sz.y = img.height; 399 applyAlign(rc, sz, Align.HCenter, Align.VCenter); 400 uint st = state; 401 img.drawTo(buf, rc, st); 402 } 403 } 404 405 protected ListWidget createRootsList() { 406 ListWidget res = new ListWidget("ROOTS_LIST"); 407 res.styleId = STYLE_LIST_BOX; 408 WidgetListAdapter adapter = new WidgetListAdapter(); 409 foreach(ref RootEntry root; _roots) { 410 ImageTextButton btn = new ImageTextButton(null, root.icon, root.label); 411 static if (BACKEND_CONSOLE) btn.margins = Rect(1, 1, 0, 0); 412 btn.orientation = Orientation.Vertical; 413 btn.styleId = STYLE_TRANSPARENT_BUTTON_BACKGROUND; 414 btn.focusable = false; 415 btn.tooltipText = root.path.toUTF32; 416 adapter.add(btn); 417 } 418 res.ownAdapter = adapter; 419 res.layoutWidth(WRAP_CONTENT).layoutHeight(FILL_PARENT).layoutWeight(0); 420 res.itemClick = delegate(Widget source, int itemIndex) { 421 openDirectory(_roots[itemIndex].path, null); 422 res.selectItem(-1); 423 return true; 424 }; 425 res.focusable = true; 426 debug Log.d("root lisk styleId=", res.styleId); 427 return res; 428 } 429 430 /// file list item activated (double clicked or Enter key pressed) 431 protected void onItemActivated(int index) { 432 DirEntry e = _entries[index]; 433 if (e.isDir) { 434 openDirectory(e.name, _path); 435 } else if (e.isFile) { 436 string fname = e.name; 437 Action result = _action; 438 result.stringParam = fname; 439 close(result); 440 } 441 442 } 443 444 /// file list item selected 445 protected void onItemSelected(int index) { 446 DirEntry e = _entries[index]; 447 string fname = e.name; 448 _edFilename.text = toUTF32(baseName(fname)); 449 _filename = fname; 450 } 451 452 protected void createAndEnterDirectory(string name) { 453 string newdir = buildNormalizedPath(_path, name); 454 try { 455 mkdirRecurse(newdir); 456 openDirectory(newdir, null); 457 } catch (Exception e) { 458 window.showMessageBox(UIString.fromId("CREATE_FOLDER_ERROR_TITLE"c), UIString.fromId("CREATE_FOLDER_ERROR_MESSAGE"c)); 459 } 460 } 461 462 /// Custom handling of actions 463 override bool handleAction(const Action action) { 464 if (action.id == StandardAction.Cancel) { 465 super.handleAction(action); 466 return true; 467 } 468 if (action.id == FileDialogActions.ShowInFileManager) { 469 Platform.instance.showInFileManager(action.stringParam); 470 return true; 471 } 472 if (action.id == StandardAction.CreateDirectory) { 473 // show editor popup 474 window.showInputBox(UIString.fromId("CREATE_NEW_FOLDER"c), UIString.fromId("INPUT_NAME_FOR_FOLDER"c), ""d, delegate(dstring s) { 475 if (!s.empty) 476 createAndEnterDirectory(toUTF8(s)); 477 }); 478 return true; 479 } 480 if (action.id == StandardAction.Open || action.id == StandardAction.OpenDirectory || action.id == StandardAction.Save) { 481 auto baseFilename = toUTF8(_edFilename.text); 482 _filename = _path ~ dirSeparator ~ baseFilename; 483 484 if (action.id != StandardAction.OpenDirectory && exists(_filename) && isDir(_filename)) { 485 auto row = _fileList.row(); 486 onItemActivated(row); 487 return true; 488 } else if (baseFilename.length > 0) { 489 Action result = _action; 490 result.stringParam = _filename; 491 // success if either selected dir & has to open dir or if selected file 492 if (action.id == StandardAction.OpenDirectory && exists(_filename) && isDir(_filename) || 493 action.id == StandardAction.Save && !(_flags & FileDialogFlag.FileMustExist) || 494 exists(_filename) && isFile(_filename)) { 495 close(result); 496 return true; 497 } 498 } 499 } 500 return super.handleAction(action); 501 } 502 503 bool onPathSelected(string path) { 504 // 505 return openDirectory(path, null); 506 } 507 508 protected MenuItem getCellPopupMenu(GridWidgetBase source, int col, int row) { 509 if (row >= 0 && row < _entries.length) { 510 MenuItem item = new MenuItem(); 511 DirEntry e = _entries[row]; 512 // show in explorer action 513 auto showAction = new Action(FileDialogActions.ShowInFileManager, "ACTION_FILE_SHOW_IN_FILE_MANAGER"c); 514 showAction.stringParam = e.name; 515 item.add(showAction); 516 // create directory action 517 if (_flags & FileDialogFlag.EnableCreateDirectory) 518 item.add(ACTION_CREATE_DIRECTORY); 519 520 if (e.isDir) { 521 //_edFilename.text = ""d; 522 //_filename = ""; 523 } else if (e.isFile) { 524 //string fname = e.name; 525 //_edFilename.text = toUTF32(baseName(fname)); 526 //_filename = fname; 527 } 528 return item; 529 } 530 return null; 531 } 532 533 /// override to implement creation of dialog controls 534 override void initialize() { 535 _roots = getRootPaths() ~ getBookmarkPaths(); 536 537 layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).minWidth(BACKEND_CONSOLE ? 50 : 600); 538 //minHeight = 400; 539 540 LinearLayout content = new HorizontalLayout("dlgcontent"); 541 542 content.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); //.minWidth(400).minHeight(300); 543 544 545 //leftPanel = new VerticalLayout("places"); 546 //leftPanel.addChild(createRootsList()); 547 //leftPanel.layoutHeight(FILL_PARENT).minWidth(BACKEND_CONSOLE ? 7 : 40); 548 549 leftPanel = createRootsList(); 550 leftPanel.minWidth(BACKEND_CONSOLE ? 7 : 40.pointsToPixels); 551 552 rightPanel = new VerticalLayout("main"); 553 rightPanel.layoutHeight(FILL_PARENT).layoutWidth(FILL_PARENT); 554 rightPanel.addChild(new TextWidget(null, "Path:"d)); 555 556 content.addChild(leftPanel); 557 content.addChild(rightPanel); 558 559 _edPath = new FilePathPanel("path"); 560 _edPath.layoutWidth(FILL_PARENT); 561 _edPath.layoutWeight = 0; 562 _edPath.onPathSelectionListener = &onPathSelected; 563 HorizontalLayout fnlayout = new HorizontalLayout(); 564 fnlayout.layoutWidth(FILL_PARENT); 565 _edFilename = new EditLine("filename"); 566 _edFilename.layoutWidth(FILL_PARENT); 567 if (_flags & FileDialogFlag.SelectDirectory) { 568 _edFilename.visibility = Visibility.Gone; 569 } 570 571 //_edFilename.layoutWeight = 0; 572 fnlayout.addChild(_edFilename); 573 if (_filters.length) { 574 dstring[] filterLabels; 575 foreach(f; _filters) 576 filterLabels ~= f.label.value; 577 _cbFilters = new ComboBox("filter", filterLabels); 578 _cbFilters.selectedItemIndex = _filterIndex; 579 _cbFilters.itemClick = delegate(Widget source, int itemIndex) { 580 _filterIndex = itemIndex; 581 reopenDirectory(); 582 return true; 583 }; 584 _cbFilters.layoutWidth(WRAP_CONTENT); 585 _cbFilters.layoutWeight(0); 586 //_cbFilters.backgroundColor = 0xFFC0FF; 587 fnlayout.addChild(_cbFilters); 588 //fnlayout.backgroundColor = 0xFFFFC0; 589 } 590 591 _fileList = new StringGridWidget("files"); 592 _fileList.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); 593 _fileList.resize(4, 3); 594 _fileList.setColTitle(0, " "d); 595 _fileList.setColTitle(1, "Name"d); 596 _fileList.setColTitle(2, "Size"d); 597 _fileList.setColTitle(3, "Modified"d); 598 _fileList.showRowHeaders = false; 599 _fileList.rowSelect = true; 600 _fileList.multiSelect = _allowMultipleFiles; 601 _fileList.cellPopupMenu = &getCellPopupMenu; 602 _fileList.menuItemAction = &handleAction; 603 604 _fileList.keyEvent = delegate(Widget source, KeyEvent event) { 605 if (_shortcutHelper.onKeyEvent(event)) 606 locateFileInList(_shortcutHelper.text); 607 return false; 608 }; 609 610 rightPanel.addChild(_edPath); 611 rightPanel.addChild(_fileList); 612 rightPanel.addChild(fnlayout); 613 614 615 addChild(content); 616 if (_flags & FileDialogFlag.EnableCreateDirectory) { 617 addChild(createButtonsPanel([ACTION_CREATE_DIRECTORY, cast(immutable)_action, ACTION_CANCEL], 1, 1)); 618 } else { 619 addChild(createButtonsPanel([cast(immutable)_action, ACTION_CANCEL], 0, 0)); 620 } 621 622 _fileList.customCellAdapter = this; 623 _fileList.cellActivated = delegate(GridWidgetBase source, int col, int row) { 624 onItemActivated(row); 625 }; 626 _fileList.cellSelected = delegate(GridWidgetBase source, int col, int row) { 627 onItemSelected(row); 628 }; 629 630 if (_path.empty) { 631 _path = currentDir; 632 } 633 openDirectory(_path, _filename); 634 _fileList.layoutHeight = FILL_PARENT; 635 636 } 637 638 protected TextTypingShortcutHelper _shortcutHelper; 639 640 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 641 override void layout(Rect rc) { 642 super.layout(rc); 643 autofitGrid(); 644 } 645 646 647 ///// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 648 //override void measure(int parentWidth, int parentHeight) { 649 // super.measure(parentWidth, parentHeight); 650 // for(int i = 0; i < childCount; i++) { 651 // Widget w = child(i); 652 // Log.d("id=", w.id, " measuredHeight=", w.measuredHeight ); 653 // for (int j = 0; j < w.childCount; j++) { 654 // Widget w2 = w.child(j); 655 // Log.d(" id=", w2.id, " measuredHeight=", w.measuredHeight ); 656 // } 657 // } 658 // Log.d("this id=", id, " measuredHeight=", measuredHeight); 659 //} 660 661 override void onShow() { 662 _fileList.setFocus(); 663 } 664 } 665 666 interface OnPathSelectionHandler { 667 bool onPathSelected(string path); 668 } 669 670 class FilePathPanelItem : HorizontalLayout { 671 protected string _path; 672 protected TextWidget _text; 673 protected ImageButton _button; 674 Listener!OnPathSelectionHandler onPathSelectionListener; 675 this(string path) { 676 super(null); 677 styleId = STYLE_LIST_ITEM; 678 _path = path; 679 string fname = isRoot(path) ? path : baseName(path); 680 _text = new TextWidget(null, toUTF32(fname)); 681 _text.styleId = STYLE_BUTTON_TRANSPARENT; 682 _text.clickable = true; 683 _text.click = &onTextClick; 684 //_text.backgroundColor = 0xC0FFFF; 685 _text.state = State.Parent; 686 _button = new ImageButton(null, ATTR_SCROLLBAR_BUTTON_RIGHT); 687 _button.styleId = STYLE_BUTTON_TRANSPARENT; 688 _button.focusable = false; 689 _button.click = &onButtonClick; 690 //_button.backgroundColor = 0xC0FFC0; 691 _button.state = State.Parent; 692 trackHover(true); 693 addChild(_text); 694 addChild(_button); 695 margins(Rect(2.pointsToPixels + 1, 0, 2.pointsToPixels + 1, 0)); 696 } 697 private bool onTextClick(Widget src) { 698 if (onPathSelectionListener.assigned) 699 return onPathSelectionListener(_path); 700 return false; 701 } 702 private bool onButtonClick(Widget src) { 703 // show popup menu with subdirs 704 string[] filters; 705 DirEntry[] entries; 706 try { 707 AttrFilter attrFilter = AttrFilter.dirs | AttrFilter.parent; 708 entries = listDirectory(_path, attrFilter); 709 } catch(Exception e) { 710 return false; 711 } 712 if (entries.length == 0) 713 return false; 714 MenuItem dirs = new MenuItem(); 715 int itemId = 25000; 716 foreach(ref DirEntry e; entries) { 717 string fullPath = e.name; 718 string d = baseName(fullPath); 719 Action a = new Action(itemId++, toUTF32(d)); 720 a.stringParam = fullPath; 721 MenuItem item = new MenuItem(a); 722 item.menuItemAction = delegate(const Action action) { 723 if (onPathSelectionListener.assigned) 724 return onPathSelectionListener(action.stringParam); 725 return false; 726 }; 727 dirs.add(item); 728 } 729 PopupMenu menuWidget = new PopupMenu(dirs); 730 PopupWidget popup = window.showPopup(menuWidget, this, PopupAlign.Below); 731 popup.flags = PopupFlags.CloseOnClickOutside; 732 return true; 733 } 734 } 735 736 /// Panel with buttons - path segments - for fast navigation to subdirs. 737 class FilePathPanelButtons : WidgetGroupDefaultDrawing { 738 protected string _path; 739 Listener!OnPathSelectionHandler onPathSelectionListener; 740 protected bool onPathSelected(string path) { 741 if (onPathSelectionListener.assigned) { 742 return onPathSelectionListener(path); 743 } 744 return false; 745 } 746 this(string ID = null) { 747 super(ID); 748 layoutWidth = FILL_PARENT; 749 clickable = true; 750 } 751 protected void initialize(string path) { 752 _path = path; 753 _children.clear(); 754 string itemPath = path; 755 for (;;) { 756 FilePathPanelItem item = new FilePathPanelItem(itemPath); 757 item.onPathSelectionListener = &onPathSelected; 758 addChild(item); 759 if (isRoot(itemPath)) { 760 break; 761 } 762 itemPath = parentDir(itemPath); 763 } 764 } 765 /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 766 override void measure(int parentWidth, int parentHeight) { 767 Rect m = margins; 768 Rect p = padding; 769 // calc size constraints for children 770 int pwidth = parentWidth; 771 int pheight = parentHeight; 772 if (parentWidth != SIZE_UNSPECIFIED) 773 pwidth -= m.left + m.right + p.left + p.right; 774 if (parentHeight != SIZE_UNSPECIFIED) 775 pheight -= m.top + m.bottom + p.top + p.bottom; 776 int reservedForEmptySpace = parentWidth / 20; 777 if (reservedForEmptySpace > 40.pointsToPixels) 778 reservedForEmptySpace = 40.pointsToPixels; 779 if (reservedForEmptySpace < 4.pointsToPixels) 780 reservedForEmptySpace = 4.pointsToPixels; 781 782 Point sz; 783 sz.x += reservedForEmptySpace; 784 // measure children 785 bool exceeded = false; 786 for (int i = 0; i < _children.count; i++) { 787 Widget item = _children.get(i); 788 item.visibility = Visibility.Visible; 789 item.measure(pwidth, pheight); 790 if (sz.y < item.measuredHeight) 791 sz.y = item.measuredHeight; 792 if (sz.x + item.measuredWidth > pwidth) { 793 exceeded = true; 794 } 795 if (!exceeded || i == 0) // at least one item must be visible 796 sz.x += item.measuredWidth; 797 else 798 item.visibility = Visibility.Gone; 799 } 800 measuredContent(parentWidth, parentHeight, sz.x, sz.y); 801 } 802 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 803 override void layout(Rect rc) { 804 //Log.d("tabControl.layout enter"); 805 _needLayout = false; 806 if (visibility == Visibility.Gone) { 807 return; 808 } 809 _pos = rc; 810 applyMargins(rc); 811 applyPadding(rc); 812 813 int reservedForEmptySpace = rc.width / 20; 814 if (reservedForEmptySpace > 40) 815 reservedForEmptySpace = 40; 816 if (reservedForEmptySpace < 4) 817 reservedForEmptySpace = 4; 818 int maxw = rc.width - reservedForEmptySpace; 819 int totalw = 0; 820 int visibleItems = 0; 821 bool exceeded = false; 822 // measure and update visibility 823 for (int i = 0; i < _children.count; i++) { 824 Widget item = _children.get(i); 825 item.visibility = Visibility.Visible; 826 item.measure(rc.width, rc.height); 827 if (totalw + item.measuredWidth > rc.width) { 828 exceeded = true; 829 } 830 if (!exceeded || i == 0) { // at least one item must be visible 831 totalw += item.measuredWidth; 832 visibleItems++; 833 } else 834 item.visibility = Visibility.Gone; 835 } 836 // layout visible items 837 // backward order 838 Rect itemRect = rc; 839 for (int i = visibleItems - 1; i >= 0; i--) { 840 Widget item = _children.get(i); 841 int w = item.measuredWidth; 842 if (i == visibleItems - 1 && w > maxw) 843 w = maxw; 844 itemRect.right = itemRect.left + w; 845 item.layout(itemRect); 846 itemRect.left += w; 847 } 848 849 } 850 851 852 } 853 854 interface PathSelectedHandler { 855 bool onPathSelected(string path); 856 } 857 858 /// Panel - either path segment buttons or text editor line 859 class FilePathPanel : FrameLayout { 860 Listener!OnPathSelectionHandler onPathSelectionListener; 861 static const ID_SEGMENTS = "SEGMENTS"; 862 static const ID_EDITOR = "ED_PATH"; 863 protected FilePathPanelButtons _segments; 864 protected EditLine _edPath; 865 protected string _path; 866 Signal!PathSelectedHandler pathListener; 867 this(string ID = null) { 868 super(ID); 869 _segments = new FilePathPanelButtons(ID_SEGMENTS); 870 _edPath = new EditLine(ID_EDITOR); 871 _edPath.layoutWidth = FILL_PARENT; 872 _edPath.editorAction = &onEditorAction; 873 _edPath.focusChange = &onEditorFocusChanged; 874 _segments.click = &onSegmentsClickOutside; 875 _segments.onPathSelectionListener = &onPathSelected; 876 addChild(_segments); 877 addChild(_edPath); 878 } 879 protected bool onEditorFocusChanged(Widget source, bool focused) { 880 if (!focused) { 881 _edPath.text = toUTF32(_path); 882 showChild(ID_SEGMENTS); 883 } 884 return true; 885 } 886 protected bool onPathSelected(string path) { 887 if (onPathSelectionListener.assigned) { 888 if (exists(path)) 889 return onPathSelectionListener(path); 890 } 891 return false; 892 } 893 protected bool onSegmentsClickOutside(Widget w) { 894 // switch to editor 895 _edPath.text = toUTF32(_path); 896 showChild(ID_EDITOR); 897 _edPath.setFocus(); 898 return true; 899 } 900 protected bool onEditorAction(const Action action) { 901 if (action.id == EditorActions.InsertNewLine) { 902 string fn = buildNormalizedPath(toUTF8(_edPath.text)); 903 if (exists(fn) && isDir(fn)) 904 return onPathSelected(fn); 905 } 906 return false; 907 } 908 909 @property void path(string value) { 910 _segments.initialize(value); 911 _edPath.text = toUTF32(value); 912 _path = value; 913 showChild(ID_SEGMENTS); 914 } 915 @property string path() { 916 return _path; 917 } 918 } 919 920 class FileNameEditLine : HorizontalLayout { 921 protected EditLine _edFileName; 922 protected Button _btn; 923 protected string[string] _filetypeIcons; 924 protected dstring _caption = "Open File"d; 925 protected uint _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist | FileDialogFlag.EnableCreateDirectory; 926 protected FileFilterEntry[] _filters; 927 protected int _filterIndex; 928 929 /// Modified state change listener (e.g. content has been saved, or first time modified after save) 930 Signal!ModifiedStateListener modifiedStateChange; 931 /// editor content is changed 932 Signal!EditableContentChangeListener contentChange; 933 934 this(string ID = null) { 935 super(ID); 936 _edFileName = new EditLine("FileNameEditLine_edFileName"); 937 _edFileName.minWidth(BACKEND_CONSOLE ? 16 : 200); 938 _btn = new Button("FileNameEditLine_btnFile", "..."d); 939 _btn.styleId = STYLE_BUTTON_NOMARGINS; 940 _btn.layoutWeight = 0; 941 _btn.click = delegate(Widget src) { 942 FileDialog dlg = new FileDialog(UIString.fromRaw(_caption), window, null, _fileDialogFlags); 943 foreach(key, value; _filetypeIcons) 944 dlg.filetypeIcons[key] = value; 945 dlg.filters = _filters; 946 dlg.dialogResult = delegate(Dialog dlg, const Action result) { 947 if (result.id == ACTION_OPEN.id || result.id == ACTION_OPEN_DIRECTORY.id) { 948 _edFileName.text = toUTF32(result.stringParam); 949 if (contentChange.assigned) 950 contentChange(_edFileName.content); 951 } 952 }; 953 string path = toUTF8(_edFileName.text); 954 if (!path.empty) { 955 if (exists(path) && isFile(path)) { 956 dlg.path = dirName(path); 957 dlg.filename = baseName(path); 958 } else if (exists(path) && isDir(path)) { 959 dlg.path = path; 960 } 961 } 962 dlg.show(); 963 return true; 964 }; 965 _edFileName.contentChange = delegate(EditableContent content) { 966 if (contentChange.assigned) 967 contentChange(content); 968 }; 969 _edFileName.modifiedStateChange = delegate(Widget src, bool modified) { 970 if (modifiedStateChange.assigned) 971 modifiedStateChange(src, modified); 972 }; 973 addChild(_edFileName); 974 addChild(_btn); 975 } 976 977 @property uint fileDialogFlags() { return _fileDialogFlags; } 978 @property void fileDialogFlags(uint f) { _fileDialogFlags = f; } 979 980 @property dstring caption() { return _caption; } 981 @property void caption(dstring s) { _caption = s; } 982 983 /// returns widget content text (override to support this) 984 override @property dstring text() { return _edFileName.text; } 985 /// sets widget content text (override to support this) 986 override @property Widget text(dstring s) { _edFileName.text = s; return this; } 987 /// sets widget content text (override to support this) 988 override @property Widget text(UIString s) { _edFileName.text = s.value; return this; } 989 990 /// mapping of file extension to icon resource name, e.g. ".txt": "text-plain" 991 @property ref string[string] filetypeIcons() { return _filetypeIcons; } 992 993 /// filter list for file type filter combo box 994 @property FileFilterEntry[] filters() { 995 return _filters; 996 } 997 998 /// filter list for file type filter combo box 999 @property void filters(FileFilterEntry[] values) { 1000 _filters = values; 1001 } 1002 1003 /// add new filter entry 1004 void addFilter(FileFilterEntry value) { 1005 _filters ~= value; 1006 } 1007 1008 /// filter index 1009 @property int filterIndex() { 1010 return _filterIndex; 1011 } 1012 1013 /// filter index 1014 @property void filterIndex(int index) { 1015 _filterIndex = index; 1016 } 1017 1018 @property bool readOnly() { return _edFileName.readOnly; } 1019 @property void readOnly(bool f) { _edFileName.readOnly = f; } 1020 1021 } 1022 1023 class DirEditLine : FileNameEditLine { 1024 this(string ID = null) { 1025 super(ID); 1026 _fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable 1027 | FileDialogFlag.FileMustExist | FileDialogFlag.SelectDirectory | FileDialogFlag.EnableCreateDirectory; 1028 _caption = "Select directory"d; 1029 } 1030 } 1031 1032 //import dlangui.widgets.metadata; 1033 //mixin(registerWidgets!(FileNameEditLine, DirEditLine)());