dlangui/src/dlangui/dialogs/filedlg.d

670 lines
20 KiB
D

// Written in the D programming language.
/**
This module contains FileDialog implementation.
Can show dialog for open / save.
Synopsis:
----
import dlangui.dialogs.filedlg;
UIString caption = "Open File"d;
auto dlg = new FileDialog(caption, window, FileDialogFlag.Open);
dlg.show();
----
Copyright: Vadim Lopatin, 2014
License: Boost License 1.0
Authors: Vadim Lopatin, coolreader.org@gmail.com
*/
module dlangui.dialogs.filedlg;
import dlangui.core.events;
import dlangui.core.i18n;
import dlangui.core.stdaction;
import dlangui.core.files;
import dlangui.widgets.controls;
import dlangui.widgets.lists;
import dlangui.widgets.popup;
import dlangui.widgets.layouts;
import dlangui.widgets.grid;
import dlangui.widgets.editors;
import dlangui.widgets.menu;
import dlangui.widgets.combobox;
import dlangui.platforms.common.platform;
import dlangui.dialogs.dialog;
private import std.algorithm;
private import std.file;
private import std.path;
private import std.utf;
private import std.conv : to;
private import std.array : split;
/// flags for file dialog options
enum FileDialogFlag : uint {
/// file must exist (use this for open dialog)
FileMustExist = 0x100,
/// ask before saving to existing
ConfirmOverwrite = 0x200,
/// flags for Open dialog
Open = FileMustExist,
/// flags for Save dialog
Save = ConfirmOverwrite,
}
/// filetype filter entry for FileDialog
struct FileFilterEntry {
UIString label;
string[] filter;
this(UIString displayLabel, string filterList) {
label = displayLabel;
if (filterList.length)
filter = split(filterList, ";");
}
}
__gshared bool SHOW_FILE_DIALOG_IN_POPUP = true;
/// File open / save dialog
class FileDialog : Dialog, CustomGridCellAdapter {
protected FilePathPanel _edPath;
protected EditLine _edFilename;
protected ComboBox _cbFilters;
protected StringGridWidget _fileList;
protected VerticalLayout leftPanel;
protected VerticalLayout rightPanel;
protected Action _action;
protected RootEntry[] _roots;
protected FileFilterEntry[] _filters;
protected int _filterIndex;
protected string _path;
protected string _filename;
protected DirEntry[] _entries;
protected bool _isRoot;
protected bool _isOpenDialog;
protected string[string] _filetypeIcons;
this(UIString caption, Window parent, Action action = null, uint fileDialogFlags = DialogFlag.Modal | DialogFlag.Resizable | FileDialogFlag.FileMustExist) {
super(caption, parent, fileDialogFlags | (SHOW_FILE_DIALOG_IN_POPUP ? DialogFlag.Popup : 0));
_isOpenDialog = !(_flags & FileDialogFlag.ConfirmOverwrite);
if (action is null) {
if (_isOpenDialog)
action = ACTION_OPEN.clone();
else
action = ACTION_SAVE.clone();
}
_action = action;
}
/// mapping of file extension to icon resource name, e.g. ".txt": "text-plain"
@property ref string[string] filetypeIcons() { return _filetypeIcons; }
/// filter list for file type filter combo box
@property FileFilterEntry[] filters() {
return _filters;
}
/// filter list for file type filter combo box
@property void filters(FileFilterEntry[] values) {
_filters = values;
}
/// add new filter entry
void addFilter(FileFilterEntry value) {
_filters ~= value;
}
/// filter index
@property int filterIndex() {
return _filterIndex;
}
/// filter index
@property void filterIndex(int index) {
_filterIndex = index;
}
/// return currently selected filter value - array of patterns like ["*.txt", "*.rtf"]
@property string[] selectedFilter() {
if (_filterIndex >= 0 && _filterIndex < _filters.length)
return _filters[_filterIndex].filter;
return null;
}
protected bool upLevel() {
return openDirectory(parentDir(_path), _path);
}
protected bool reopenDirectory() {
return openDirectory(_path, null);
}
protected bool openDirectory(string dir, string selectedItemPath) {
dir = buildNormalizedPath(dir);
Log.d("FileDialog.openDirectory(", dir, ")");
_fileList.rows = 0;
if (!listDirectory(dir, true, true, false, selectedFilter, _entries))
return false;
_path = dir;
_isRoot = isRoot(dir);
_edPath.path = _path; //toUTF32(_path);
_fileList.rows = cast(int)_entries.length;
int selectionIndex = -1;
for (int i = 0; i < _entries.length; i++) {
if (_entries[i].name.equal(selectedItemPath))
selectionIndex = i;
string fname = baseName(_entries[i].name);
string sz;
string date;
bool d = _entries[i].isDir;
_fileList.setCellText(1, i, toUTF32(fname));
if (d) {
_fileList.setCellText(0, i, "folder");
} else {
string ext = extension(fname);
string resname;
if (ext in _filetypeIcons)
resname = _filetypeIcons[ext];
else if (baseName(fname) in _filetypeIcons)
resname = _filetypeIcons[baseName(fname)];
else
resname = "text-plain";
_fileList.setCellText(0, i, toUTF32(resname));
sz = to!string(_entries[i].size);
date = "2014-01-01 00:00:00";
}
_fileList.setCellText(2, i, toUTF32(sz));
_fileList.setCellText(3, i, toUTF32(date));
}
_fileList.autoFitColumnWidths();
_fileList.fillColumnWidth(1);
if (selectionIndex >= 0)
_fileList.selectCell(1, selectionIndex + 1, true);
else if (_entries.length > 0)
_fileList.selectCell(1, 1, true);
return true;
}
override bool onKeyEvent(KeyEvent event) {
if (event.action == KeyAction.KeyDown) {
if (event.keyCode == KeyCode.BACK && event.flags == 0) {
upLevel();
return true;
}
}
return false;
}
/// return true for custom drawn cell
override bool isCustomCell(int col, int row) {
if (col == 0 && row >= 0)
return true;
return false;
}
protected DrawableRef rowIcon(int row) {
string iconId = toUTF8(_fileList.cellText(0, row));
DrawableRef res;
if (iconId.length)
res = drawableCache.get(iconId);
return res;
}
/// return cell size
override Point measureCell(int col, int row) {
DrawableRef icon = rowIcon(row);
if (icon.isNull)
return Point(0, 0);
return Point(icon.width + 2, icon.height + 2);
}
/// draw data cell content
override void drawCell(DrawBuf buf, Rect rc, int col, int row) {
DrawableRef img = rowIcon(row);
if (!img.isNull) {
Point sz;
sz.x = img.width;
sz.y = img.height;
applyAlign(rc, sz, Align.HCenter, Align.VCenter);
uint st = state;
img.drawTo(buf, rc, st);
}
}
protected ListWidget createRootsList() {
ListWidget res = new ListWidget("ROOTS_LIST");
res.styleId = STYLE_EDIT_BOX;
WidgetListAdapter adapter = new WidgetListAdapter();
foreach(ref RootEntry root; _roots) {
ImageTextButton btn = new ImageTextButton(null, root.icon, root.label);
btn.orientation = Orientation.Vertical;
btn.styleId = STYLE_TRANSPARENT_BUTTON_BACKGROUND;
btn.focusable = false;
adapter.widgets.add(btn);
}
res.ownAdapter = adapter;
res.layoutWidth(WRAP_CONTENT).layoutHeight(FILL_PARENT).layoutWeight(0);
res.onItemClickListener = delegate(Widget source, int itemIndex) {
openDirectory(_roots[itemIndex].path, null);
res.selectItem(-1);
return true;
};
res.focusable = true;
debug Log.d("root lisk styleId=", res.styleId);
return res;
}
/// file list item activated (double clicked or Enter key pressed)
protected void onItemActivated(int index) {
DirEntry e = _entries[index];
if (e.isDir) {
openDirectory(e.name, _path);
} else if (e.isFile) {
string fname = e.name;
Action result = _action;
result.stringParam = fname;
close(result);
}
}
/// file list item selected
protected void onItemSelected(int index) {
DirEntry e = _entries[index];
if (e.isDir) {
_edFilename.text = ""d;
_filename = "";
} else if (e.isFile) {
string fname = e.name;
_edFilename.text = toUTF32(baseName(fname));
_filename = fname;
}
}
/// Custom handling of actions
override bool handleAction(const Action action) {
if (action.id == StandardAction.Cancel) {
super.handleAction(action);
return true;
}
if (action.id == StandardAction.Open || action.id == StandardAction.Save) {
if (_filename.length > 0) {
Action result = _action;
result.stringParam = _filename;
close(result);
return true;
}
}
return super.handleAction(action);
}
bool onPathSelected(string path) {
//
return openDirectory(path, null);
}
/// override to implement creation of dialog controls
override void init() {
_roots = getRootPaths;
layoutWidth(FILL_PARENT);
layoutWidth(FILL_PARENT);
minWidth = 600;
//minHeight = 400;
LinearLayout content = new HorizontalLayout("dlgcontent");
content.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); //.minWidth(400).minHeight(300);
leftPanel = new VerticalLayout("places");
leftPanel.addChild(createRootsList());
leftPanel.layoutHeight(FILL_PARENT).minWidth(40);
rightPanel = new VerticalLayout("main");
rightPanel.layoutHeight(FILL_PARENT).layoutWidth(FILL_PARENT);
rightPanel.addChild(new TextWidget(null, "Path:"d));
content.addChild(leftPanel);
content.addChild(rightPanel);
_edPath = new FilePathPanel("path");
_edPath.layoutWidth(FILL_PARENT);
_edPath.layoutWeight = 0;
_edPath.onPathSelectionListener = &onPathSelected;
HorizontalLayout fnlayout = new HorizontalLayout();
fnlayout.layoutWidth(FILL_PARENT);
_edFilename = new EditLine("filename");
_edFilename.layoutWidth(FILL_PARENT);
//_edFilename.layoutWeight = 0;
fnlayout.addChild(_edFilename);
if (_filters.length) {
dstring[] filterLabels;
foreach(f; _filters)
filterLabels ~= f.label.value;
_cbFilters = new ComboBox("filter", filterLabels);
_cbFilters.selectedItemIndex = _filterIndex;
_cbFilters.onItemClickListener = delegate(Widget source, int itemIndex) {
_filterIndex = itemIndex;
reopenDirectory();
return true;
};
_cbFilters.layoutWidth(WRAP_CONTENT);
_cbFilters.layoutWeight(0);
//_cbFilters.backgroundColor = 0xFFC0FF;
fnlayout.addChild(_cbFilters);
//fnlayout.backgroundColor = 0xFFFFC0;
}
_fileList = new StringGridWidget("files");
_fileList.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
_fileList.resize(4, 3);
_fileList.setColTitle(0, " "d);
_fileList.setColTitle(1, "Name"d);
_fileList.setColTitle(2, "Size"d);
_fileList.setColTitle(3, "Modified"d);
_fileList.showRowHeaders = false;
_fileList.rowSelect = true;
rightPanel.addChild(_edPath);
rightPanel.addChild(_fileList);
rightPanel.addChild(fnlayout);
addChild(content);
addChild(createButtonsPanel([cast(immutable)_action, ACTION_CANCEL], 0, 0));
_fileList.customCellAdapter = this;
_fileList.onCellActivated = delegate(GridWidgetBase source, int col, int row) {
onItemActivated(row);
};
_fileList.onCellSelected = delegate(GridWidgetBase source, int col, int row) {
onItemSelected(row);
};
openDirectory(currentDir, null);
_fileList.layoutHeight = FILL_PARENT;
}
/// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
override void layout(Rect rc) {
super.layout(rc);
_fileList.autoFitColumnWidths();
_fileList.fillColumnWidth(1);
}
///// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
//override void measure(int parentWidth, int parentHeight) {
// super.measure(parentWidth, parentHeight);
// for(int i = 0; i < childCount; i++) {
// Widget w = child(i);
// Log.d("id=", w.id, " measuredHeight=", w.measuredHeight );
// for (int j = 0; j < w.childCount; j++) {
// Widget w2 = w.child(j);
// Log.d(" id=", w2.id, " measuredHeight=", w.measuredHeight );
// }
// }
// Log.d("this id=", id, " measuredHeight=", measuredHeight);
//}
override void onShow() {
_fileList.setFocus();
}
}
interface OnPathSelectionHandler {
bool onPathSelected(string path);
}
class FilePathPanelItem : HorizontalLayout {
protected string _path;
protected TextWidget _text;
protected ImageButton _button;
Listener!OnPathSelectionHandler onPathSelectionListener;
this(string path) {
super(null);
styleId = STYLE_LIST_ITEM;
_path = path;
string fname = isRoot(path) ? path : baseName(path);
_text = new TextWidget(null, toUTF32(fname));
_text.styleId = STYLE_BUTTON_TRANSPARENT;
_text.clickable = true;
_text.onClickListener = &onTextClick;
//_text.backgroundColor = 0xC0FFFF;
_text.state = State.Parent;
_button = new ImageButton(null, "scrollbar_btn_right");
_button.styleId = STYLE_BUTTON_TRANSPARENT;
_button.focusable = false;
_button.onClickListener = &onButtonClick;
//_button.backgroundColor = 0xC0FFC0;
_button.state = State.Parent;
trackHover(true);
addChild(_text);
addChild(_button);
margins(Rect(2,0,2,0));
}
private bool onTextClick(Widget src) {
if (onPathSelectionListener.assigned)
return onPathSelectionListener(_path);
return false;
}
private bool onButtonClick(Widget src) {
// show popup menu with subdirs
string[] filters;
DirEntry[] entries;
if (!listDirectory(_path, true, false, false, filters, entries))
return false;
if (entries.length == 0)
return false;
MenuItem dirs = new MenuItem();
int itemId = 25000;
foreach(ref DirEntry e; entries) {
string fullPath = e.name;
string d = baseName(fullPath);
Action a = new Action(itemId++, toUTF32(d));
MenuItem item = new MenuItem(a);
item.onMenuItemClick = delegate(MenuItem item) {
if (onPathSelectionListener.assigned)
return onPathSelectionListener(fullPath);
return false;
};
dirs.add(item);
}
PopupMenu menuWidget = new PopupMenu(dirs);
PopupWidget popup = window.showPopup(menuWidget, this, PopupAlign.Below);
popup.flags = PopupFlags.CloseOnClickOutside;
return true;
}
}
/// Panel with buttons - path segments - for fast navigation to subdirs.
class FilePathPanelButtons : WidgetGroupDefaultDrawing {
protected string _path;
Listener!OnPathSelectionHandler onPathSelectionListener;
protected bool onPathSelected(string path) {
if (onPathSelectionListener.assigned) {
return onPathSelectionListener(path);
}
return false;
}
this(string ID = null) {
super(ID);
layoutWidth = FILL_PARENT;
clickable = true;
}
protected void init(string path) {
_path = path;
_children.clear();
string itemPath = path;
for (;;) {
FilePathPanelItem item = new FilePathPanelItem(itemPath);
item.onPathSelectionListener = &onPathSelected;
addChild(item);
if (isRoot(itemPath)) {
break;
}
itemPath = parentDir(itemPath);
}
}
/// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
override void measure(int parentWidth, int parentHeight) {
Rect m = margins;
Rect p = padding;
// calc size constraints for children
int pwidth = parentWidth;
int pheight = parentHeight;
if (parentWidth != SIZE_UNSPECIFIED)
pwidth -= m.left + m.right + p.left + p.right;
if (parentHeight != SIZE_UNSPECIFIED)
pheight -= m.top + m.bottom + p.top + p.bottom;
int reservedForEmptySpace = parentWidth / 20;
if (reservedForEmptySpace > 40)
reservedForEmptySpace = 40;
Point sz;
sz.x += reservedForEmptySpace;
// measure children
bool exceeded = false;
for (int i = 0; i < _children.count; i++) {
Widget item = _children.get(i);
item.visibility = Visibility.Visible;
item.measure(pwidth, pheight);
if (sz.y < item.measuredHeight)
sz.y = item.measuredHeight;
if (sz.x + item.measuredWidth > pwidth) {
exceeded = true;
}
if (!exceeded || i == 0) // at least one item must be visible
sz.x += item.measuredWidth;
else
item.visibility = Visibility.Gone;
}
measuredContent(parentWidth, parentHeight, sz.x, sz.y);
}
/// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
override void layout(Rect rc) {
//Log.d("tabControl.layout enter");
_needLayout = false;
if (visibility == Visibility.Gone) {
return;
}
_pos = rc;
applyMargins(rc);
applyPadding(rc);
int reservedForEmptySpace = rc.width / 20;
if (reservedForEmptySpace > 40)
reservedForEmptySpace = 40;
int maxw = rc.width - reservedForEmptySpace;
int totalw = 0;
int visibleItems = 0;
bool exceeded = false;
// measure and update visibility
for (int i = 0; i < _children.count; i++) {
Widget item = _children.get(i);
item.visibility = Visibility.Visible;
item.measure(rc.width, rc.height);
if (totalw + item.measuredWidth > rc.width) {
exceeded = true;
}
if (!exceeded || i == 0) { // at least one item must be visible
totalw += item.measuredWidth;
visibleItems++;
} else
item.visibility = Visibility.Gone;
}
// layout visible items
// backward order
Rect itemRect = rc;
for (int i = visibleItems - 1; i >= 0; i--) {
Widget item = _children.get(i);
int w = item.measuredWidth;
if (i == visibleItems - 1 && w > maxw)
w = maxw;
itemRect.right = itemRect.left + w;
item.layout(itemRect);
itemRect.left += w;
}
}
}
interface PathSelectedHandler {
bool onPathSelected(string path);
}
/// Panel - either path segment buttons or text editor line
class FilePathPanel : FrameLayout {
Listener!OnPathSelectionHandler onPathSelectionListener;
static const ID_SEGMENTS = "SEGMENTS";
static const ID_EDITOR = "ED_PATH";
protected FilePathPanelButtons _segments;
protected EditLine _edPath;
protected string _path;
Signal!PathSelectedHandler pathListener;
this(string ID = null) {
super(ID);
_segments = new FilePathPanelButtons(ID_SEGMENTS);
_edPath = new EditLine(ID_EDITOR);
_edPath.layoutWidth = FILL_PARENT;
_edPath.editorActionListener = &onEditorAction;
_edPath.onFocusChangeListener = &onEditorFocusChanged;
_segments.onClickListener = &onSegmentsClickOutside;
_segments.onPathSelectionListener = &onPathSelected;
addChild(_segments);
addChild(_edPath);
}
protected bool onEditorFocusChanged(Widget source, bool focused) {
if (!focused) {
_edPath.text = toUTF32(_path);
showChild(ID_SEGMENTS);
}
return true;
}
protected bool onPathSelected(string path) {
if (onPathSelectionListener.assigned) {
if (exists(path))
return onPathSelectionListener(path);
}
return false;
}
protected bool onSegmentsClickOutside(Widget w) {
// switch to editor
_edPath.text = toUTF32(_path);
showChild(ID_EDITOR);
_edPath.setFocus();
return true;
}
protected bool onEditorAction(const Action action) {
if (action.id == EditorActions.InsertNewLine) {
string fn = buildNormalizedPath(toUTF8(_edPath.text));
if (exists(fn) && isDir(fn))
return onPathSelected(fn);
}
return false;
}
@property void path(string value) {
_segments.init(value);
_edPath.text = toUTF32(value);
_path = value;
showChild(ID_SEGMENTS);
}
@property string path() {
return _path;
}
}