diff --git a/src/dlangui/core/files.d b/src/dlangui/core/files.d index 63a77dd8..7750a922 100644 --- a/src/dlangui/core/files.d +++ b/src/dlangui/core/files.d @@ -131,29 +131,36 @@ version(OSX) {} else version(Posix) } return false; } - - private string getDeviceLabelFallback(in char[] type, in char[] fsName) + + private string getDeviceLabelFallback(in char[] type, in char[] fsName, in char[] mountDir) { import std.format : format; - import std.string : startsWith; if (type == "vboxsf") { return "VirtualBox shared folder"; } - if (fsName.startsWith("gvfsd")) { - return "GNOME virtual file system"; + if (type == "fuse.gvfsd-fuse") { + return "GNOME Virtual file system"; } - return format("%s volume", type); + return format("%s (%s)", mountDir.baseName, type); } - + private RootEntryType getDeviceRootEntryType(in char[] type) { - if (type == "iso9660") { - return RootEntryType.CDROM; - } - if (type == "vfat") { - return RootEntryType.REMOVABLE; - } - return RootEntryType.FIXED; + switch(type) + { + case "iso9660": + return RootEntryType.CDROM; + case "vfat": + return RootEntryType.REMOVABLE; + case "cifs": + case "davfs": + case "fuse.sshfs": + case "nfs": + case "nfs4": + return RootEntryType.NETWORK; + default: + return RootEntryType.FIXED; + } } } @@ -247,6 +254,7 @@ private: // do nothing } else version(linux) { import std.string : fromStringz; + import std.exception : collectException; mntent ent; char[1024] buf; @@ -267,22 +275,25 @@ private: string label; enum byLabel = "/dev/disk/by-label"; - try { - foreach(entry; dirEntries(byLabel, SpanMode.shallow)) - { - if (entry.isSymlink) { - auto normalized = buildNormalizedPath(byLabel, entry.readLink); - if (normalized == fsName) { - label = entry.name.baseName.unescapeLabel(); + if (fsName.isAbsolute) { + try { + foreach(entry; dirEntries(byLabel, SpanMode.shallow)) + { + string resolvedLink; + if (entry.isSymlink && collectException(entry.readLink, resolvedLink) is null) { + auto normalized = buildNormalizedPath(byLabel, resolvedLink); + if (normalized == fsName) { + label = entry.name.baseName.unescapeLabel(); + } } } + } catch(Exception e) { + } - } catch(Exception e) { - } if (!label.length) { - label = getDeviceLabelFallback(type, fsName); + label = getDeviceLabelFallback(type, fsName, mountDir); } auto entryType = getDeviceRootEntryType(type); res ~= RootEntry(entryType, mountDir.idup, label.toUTF32); @@ -307,7 +318,7 @@ private: continue; } - string label = getDeviceLabelFallback(type, fsName); + string label = getDeviceLabelFallback(type, fsName, mountDir); res ~= RootEntry(getDeviceRootEntryType(type), mountDir.idup, label.toUTF32); } } @@ -515,6 +526,7 @@ bool isHidden(in string path) nothrow { return false; } } else version(Posix) { + //TODO: check for hidden attribute on macOS return path.baseName.startsWith("."); } else { return false; @@ -530,6 +542,49 @@ unittest } } +private bool isReadable(in string filePath) nothrow +{ + version(Posix) { + import core.sys.posix.unistd : access, R_OK; + import std.string : toStringz; + return access(toStringz(filePath), R_OK) == 0; + } else { + // TODO: Windows version + return true; + } +} + +private bool isWritable(in string filePath) nothrow +{ + version(Posix) { + import core.sys.posix.unistd : access, W_OK; + import std.string : toStringz; + return access(toStringz(filePath), W_OK) == 0; + } else { + // TODO: Windows version + return true; + } +} + +private bool isExecutable(in string filePath) nothrow +{ + version(Windows) { + //TODO: Use GetEffectiveRightsFromAclW? For now just check extension + string extension = filePath.extension; + foreach(ext; [".exe", ".com", ".bat", ".cmd"]) { + if (filenameCmp(extension, ext) == 0) + return true; + } + return false; + } else version(Posix) { + import core.sys.posix.unistd : access, X_OK; + import std.string : toStringz; + return access(toStringz(filePath), X_OK) == 0; + } else { + return false; + } +} + /// returns parent directory for specified path string parentDir(in string path) pure nothrow { return buildNormalizedPath(path, ".."); @@ -550,71 +605,92 @@ bool filterFilename(in string filename, in string[] filters) pure nothrow { return false; } -/** List directory content - - Optionally filters file names by filter. +enum AttrFilter +{ + none = 0, + files = 1 << 0, /// Include regular files that match the filters. + dirs = 1 << 1, /// Include directories. + hidden = 1 << 2, /// Include hidden files and directoroies. + parent = 1 << 3, /// Include parent directory (..). Takes effect only with includeDirs. + thisDir = 1 << 4, /// Include this directory (.). Takes effect only with includeDirs. + special = 1 << 5, /// Include special files (On Unix: socket and device files, FIFO) that match the filters. + readable = 1 << 6, /// Listing only readable files and directories. + writable = 1 << 7, /// Listing only writable files and directories. + executable = 1 << 8, /// Include only executable files. This filter does not affect directories. + allVisible = AttrFilter.files | AttrFilter.dirs, /// Include all non-hidden files and directories without parent directory, this directory and special files. + all = AttrFilter.allVisible | AttrFilter.hidden /// Include all files and directories including hidden ones but without parent directory, this directory and special files. +} + +/** List directory content + + Optionally filters file names by filter (not applied to directories). + + Returns true if directory exists and listed successfully, false otherwise. + Throws: Exception if $(D dir) is not directory or some error occured during directory listing. + */ +DirEntry[] listDirectory(in string dir, AttrFilter attrFilter = AttrFilter.all, in string[] filters = []) +{ + DirEntry[] entries; + + DirEntry[] dirs; + DirEntry[] files; + foreach (DirEntry e; dirEntries(dir, SpanMode.shallow)) { + if (!(attrFilter & AttrFilter.hidden) && e.name.isHidden()) + continue; + if ((attrFilter & AttrFilter.readable) && !e.name.isReadable()) + continue; + if ((attrFilter & AttrFilter.writable) && !e.name.isWritable()) + continue; + if (!e.isDir && (attrFilter & AttrFilter.executable) && !e.name.isExecutable()) + continue; + if (e.isDir && (attrFilter & AttrFilter.dirs)) { + dirs ~= e; + } else if ((attrFilter & AttrFilter.files) && filterFilename(e.name, filters)) { + if (e.isFile) { + files ~= e; + } else if (attrFilter & AttrFilter.special) { + files ~= e; + } + } + } + if ((attrFilter & AttrFilter.dirs) && (attrFilter & AttrFilter.thisDir) ) { + entries ~= DirEntry(appendPath(dir, ".")) ~ entries; + } + if (!isRoot(dir) && (attrFilter & AttrFilter.dirs) && (attrFilter & AttrFilter.parent)) { + entries ~= DirEntry(appendPath(dir, "..")); + } + dirs.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0); + files.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0); + entries ~= dirs; + entries ~= files; + return entries; +} + +/** List directory content + + Optionally filters file names by filter (not applied to directories). Result will be placed into entries array. Returns true if directory exists and listed successfully, false otherwise. */ -bool listDirectory(in string dir, in bool includeDirs, in bool includeFiles, in bool showHiddenFiles, in string[] filters, ref DirEntry[] entries, in bool showExecutables = false) { +deprecated bool listDirectory(in string dir, in bool includeDirs, in bool includeFiles, in bool showHiddenFiles, in string[] filters, ref DirEntry[] entries, in bool showExecutables = false) { entries.length = 0; - + + AttrFilter attrFilter; + if (includeDirs) { + attrFilter |= AttrFilter.dirs; + attrFilter |= AttrFilter.parent; + } + if (includeFiles) + attrFilter |= AttrFilter.files; + if (showHiddenFiles) + attrFilter |= AttrFilter.hidden; + if (showExecutables) + attrFilter |= AttrFilter.executable; + import std.exception : collectException; - bool dirExists; - collectException(dir.isDir, dirExists); - if (!dirExists) { - return false; - } - - if (!isRoot(dir) && includeDirs) { - entries ~= DirEntry(appendPath(dir, "..")); - } - - try { - DirEntry[] dirs; - DirEntry[] files; - foreach (DirEntry e; dirEntries(dir, SpanMode.shallow)) { - if (!showHiddenFiles && e.name.isHidden()) - continue; - if (e.isDir) { - dirs ~= e; - } else if (e.isFile) { - files ~= e; - } - } - dirs.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0); - files.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a.name,b.name) < 0); - if (includeDirs) - foreach(DirEntry e; dirs) - entries ~= e; - if (includeFiles) - foreach(DirEntry e; files) { - bool passed = false; - if (showExecutables) { - uint attr_mask = (1 << 0) | (1 << 3) | (1 << 6); - version(Windows) { - passed = e.name.endsWith(".exe") || e.name.endsWith(".EXE") - || e.name.endsWith(".cmd") || e.name.endsWith(".CMD") - || e.name.endsWith(".bat") || e.name.endsWith(".BAT"); - } else version (Posix) { - // execute permission for others - passed = (e.attributes & attr_mask) != 0; - } else version(OSX) { - passed = (e.attributes & attr_mask) != 0; - } - } else { - passed = filterFilename(e.name, filters); - } - if (passed) - entries ~= e; - } - return true; - } catch (FileException e) { - return false; - } - + return collectException(listDirectory(dir, attrFilter, filters), entries) is null; } /// Returns true if char ch is / or \ slash diff --git a/src/dlangui/dialogs/filedlg.d b/src/dlangui/dialogs/filedlg.d index ecbc7bf9..3d738f4a 100644 --- a/src/dlangui/dialogs/filedlg.d +++ b/src/dlangui/dialogs/filedlg.d @@ -256,9 +256,21 @@ class FileDialog : Dialog, CustomGridCellAdapter { protected bool openDirectory(string dir, string selectedItemPath) { dir = buildNormalizedPath(dir); Log.d("FileDialog.openDirectory(", dir, ")"); - _fileList.rows = 0; - if (!listDirectory(dir, true, true, _showHiddenFiles, selectedFilter, _entries, executableFilterSelected)) + DirEntry[] entries; + + auto attrFilter = (showHiddenFiles ? AttrFilter.all : AttrFilter.allVisible) | AttrFilter.special | AttrFilter.parent; + if (executableFilterSelected()) { + attrFilter |= AttrFilter.executable; + } + try { + _entries = listDirectory(dir, attrFilter, selectedFilter()); + } catch(Exception e) { + import dlangui.dialogs.msgbox; + auto msgBox = new MessageBox(UIString("Error"d), UIString(e.msg.toUTF32), window()); + msgBox.show(); return false; + } + _fileList.rows = 0; _path = dir; _isRoot = isRoot(dir); _edPath.path = _path; //toUTF32(_path); @@ -689,8 +701,12 @@ class FilePathPanelItem : HorizontalLayout { // show popup menu with subdirs string[] filters; DirEntry[] entries; - if (!listDirectory(_path, true, false, false, filters, entries)) + try { + AttrFilter attrFilter = AttrFilter.dirs | AttrFilter.parent; + entries = listDirectory(_path, attrFilter); + } catch(Exception e) { return false; + } if (entries.length == 0) return false; MenuItem dirs = new MenuItem(); @@ -699,10 +715,11 @@ class FilePathPanelItem : HorizontalLayout { string fullPath = e.name; string d = baseName(fullPath); Action a = new Action(itemId++, toUTF32(d)); + a.stringParam = fullPath; MenuItem item = new MenuItem(a); - item.menuItemClick = delegate(MenuItem item) { + item.menuItemAction = delegate(const Action action) { if (onPathSelectionListener.assigned) - return onPathSelectionListener(fullPath); + return onPathSelectionListener(action.stringParam); return false; }; dirs.add(item); diff --git a/src/dlangui/platforms/common/startup.d b/src/dlangui/platforms/common/startup.d index 9d01da1c..62dfd378 100644 --- a/src/dlangui/platforms/common/startup.d +++ b/src/dlangui/platforms/common/startup.d @@ -113,8 +113,11 @@ version (Windows) { import dlangui.core.files; import std.file : DirEntry; DirEntry[] entries; - if (!listDirectory(dir, false, true, true, ["*.ttf"], entries)) - return null; + try { + entries = listDirectory(dir, AttrFilter.files, ["*.ttf"]); + } catch(Exception e) { + return null; + } string[] res; foreach(entry; entries) {