diff --git a/examples/example1/src/main.d b/examples/example1/src/main.d index 8c26a4ea..b4717d58 100644 --- a/examples/example1/src/main.d +++ b/examples/example1/src/main.d @@ -717,5 +717,10 @@ extern (C) int UIAppMain(string[] args) { window.show(); //window.windowCaption = "New Window Caption"; // run message loop + + Log.i("HOME path: ", homePath); + Log.i("APPDATA path: ", appDataPath(".dlangui")); + Log.i("Root paths: ", getRootPaths); + return Platform.instance.enterMessageLoop(); } diff --git a/res/mdpi/computer.png b/res/mdpi/computer.png new file mode 100644 index 00000000..a5ee1956 Binary files /dev/null and b/res/mdpi/computer.png differ diff --git a/res/mdpi/drive-harddisk.png b/res/mdpi/drive-harddisk.png new file mode 100644 index 00000000..0ae47f10 Binary files /dev/null and b/res/mdpi/drive-harddisk.png differ diff --git a/res/mdpi/drive-optical.png b/res/mdpi/drive-optical.png new file mode 100644 index 00000000..74ff49d3 Binary files /dev/null and b/res/mdpi/drive-optical.png differ diff --git a/res/mdpi/drive-removable-media.png b/res/mdpi/drive-removable-media.png new file mode 100644 index 00000000..1400e98a Binary files /dev/null and b/res/mdpi/drive-removable-media.png differ diff --git a/res/mdpi/folder-blue.png b/res/mdpi/folder-blue.png new file mode 100644 index 00000000..5d41de26 Binary files /dev/null and b/res/mdpi/folder-blue.png differ diff --git a/res/mdpi/folder-bookmark.png b/res/mdpi/folder-bookmark.png new file mode 100644 index 00000000..b064712a Binary files /dev/null and b/res/mdpi/folder-bookmark.png differ diff --git a/res/mdpi/folder-network.png b/res/mdpi/folder-network.png new file mode 100644 index 00000000..bacca2fe Binary files /dev/null and b/res/mdpi/folder-network.png differ diff --git a/res/mdpi/media-flash-sd-mmc.png b/res/mdpi/media-flash-sd-mmc.png new file mode 100644 index 00000000..031240d7 Binary files /dev/null and b/res/mdpi/media-flash-sd-mmc.png differ diff --git a/res/mdpi/user-home.png b/res/mdpi/user-home.png new file mode 100644 index 00000000..d855860b Binary files /dev/null and b/res/mdpi/user-home.png differ diff --git a/src/dlangui/core/files.d b/src/dlangui/core/files.d new file mode 100644 index 00000000..22e8c4f4 --- /dev/null +++ b/src/dlangui/core/files.d @@ -0,0 +1,253 @@ +// Written in the D programming language. + +/** + +This module contains cross-platform file access utilities + + + +Synopsis: + +---- +import dlangui.core.files; +---- + +Copyright: Vadim Lopatin, 2014 +License: Boost License 1.0 +Authors: Vadim Lopatin, coolreader.org@gmail.com +*/ +module dlangui.core.files; + +import std.algorithm; + +private import dlangui.core.logger; +private import std.process; +private import std.path; +private import std.file; +private import std.utf; + +version (Windows) { + /// path delimiter (\ for windows, / for others) + immutable char PATH_DELIMITER = '\\'; +} else { + /// path delimiter (\ for windows, / for others) + immutable char PATH_DELIMITER = '/'; +} + +/// Filesystem root entry / bookmark types +enum RootEntryType : uint { + /// filesystem root + ROOT, + /// current user home + HOME, + /// removable drive + REMOVABLE, + /// fixed drive + FIXED, + /// network + NETWORK, + /// cd rom + CDROM, + /// sd card + SDCARD, + /// custom bookmark + BOOKMARK, +} + +/// Filesystem root entry item +struct RootEntry { + private RootEntryType _type; + private string _path; + private dstring _display; + this(RootEntryType type, string path, dstring display = null) { + _type = type; + _path = path; + _display = display; + if (display is null) { + _display = toUTF32(baseName(path)); + } + } + /// Returns type + @property RootEntryType type() { return _type; } + /// Returns path + @property string path() { return _path; } + /// Returns display label + @property dstring label() { return _display; } + /// Returns icon resource id + @property string icon() { + switch (type) { + case RootEntryType.NETWORK: + return "folder-network"; + case RootEntryType.BOOKMARK: + return "folder-bookmark"; + case RootEntryType.CDROM: + return "drive-optical"; + case RootEntryType.FIXED: + return "drive-harddisk"; + case RootEntryType.HOME: + return "user-home"; + case RootEntryType.ROOT: + return "computer"; + case RootEntryType.SDCARD: + return "media-flash-sd-mmc"; + case RootEntryType.REMOVABLE: + return "device-removable-media"; + default: + return "folder-blue"; + } + } +} + +/// Returns +@property RootEntry homeEntry() { + return RootEntry(RootEntryType.HOME, homePath); +} + +/// returns array of system root entries +@property RootEntry[] getRootPaths() { + RootEntry[] res; + res ~= RootEntry(RootEntryType.HOME, homePath); + version (posix) { + res ~= RootEntry(RootEntryType.ROOT, "/", "File System"d); + } + version (Windows) { + import win32.windows; + uint mask = GetLogicalDrives(); + for (int i = 0; i < 26; i++) { + if (mask & (1 << i)) { + char letter = cast(char)('A' + i); + string path = "" ~ letter ~ ":\\"; + dstring display = ""d ~ letter ~ ":"d; + // detect drive type + RootEntryType type; + uint wtype = GetDriveTypeA(("" ~ path).ptr); + //Log.d("Drive ", path, " type ", wtype); + switch (wtype) { + case DRIVE_REMOVABLE: + type = RootEntryType.REMOVABLE; + break; + case DRIVE_REMOTE: + type = RootEntryType.NETWORK; + break; + case DRIVE_CDROM: + type = RootEntryType.CDROM; + break; + default: + type = RootEntryType.FIXED; + break; + } + res ~= RootEntry(type, path, display); + } + } + } + return res; +} + +/** Returns true if char ch is / or \ slash */ +bool isPathDelimiter(char ch) { + return ch == '/' || ch == '\\'; +} + +/** Returns current executable path only, including last path delimiter - removes executable name from result of std.file.thisExePath() */ +@property string exePath() { + string path = thisExePath(); + int lastSlash = 0; + for (int i = 0; i < path.length; i++) + if (path[i] == PATH_DELIMITER) + lastSlash = i; + return path[0 .. lastSlash + 1]; +} + +/// Returns user's home directory +@property string homePath() { + string path; + version (Windows) { + path = environment.get("USERPROFILE"); + if (path is null) + path = environment.get("HOME"); + } else { + path = environment.get("HOME"); + } + if (path is null) + path = "."; // fallback to current directory + return path; +} + +/** + + Returns application data directory + + On unix, it will return path to subdirectory in home directory - e.g. /home/user/.subdir if ".subdir" is passed as a paramter. + + On windows, it will return path to subdir in APPDATA directory - e.g. C:\Users\User\AppData\Roaming\.subdir. + + */ +string appDataPath(string subdir = null) { + string path; + version (Windows) { + path = environment.get("APPDATA"); + } + if (path is null) + path = homePath; + if (subdir !is null) { + path ~= PATH_DELIMITER; + path ~= subdir; + } + return path; +} + +/// Converts path delimiters to standard for platform inplace in buffer(e.g. / to \ on windows, \ to / on posix), returns buf +char[] convertPathDelimiters(char[] buf) { + foreach(ref ch; buf) { + version (Windows) { + if (ch == '/') + ch = '\\'; + } else { + if (ch == '\\') + ch = '/'; + } + } + return buf; +} + +/** Converts path delimiters to standard for platform (e.g. / to \ on windows, \ to / on posix) */ +string convertPathDelimiters(string src) { + char[] buf = src.dup; + return cast(string)convertPathDelimiters(buf); +} + +/** Appends file path parts with proper delimiters e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" */ +string appendPath(string[] pathItems ...) { + char[] buf; + foreach (s; pathItems) { + if (buf.length && !isPathDelimiter(buf[$-1])) + buf ~= PATH_DELIMITER; + buf ~= s; + } + return convertPathDelimiters(buf).dup; +} + +/** Appends file path parts with proper delimiters (as well converts delimiters inside path to system) to buffer e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" */ +char[] appendPath(char[] buf, string[] pathItems ...) { + foreach (s; pathItems) { + if (buf.length && !isPathDelimiter(buf[$-1])) + buf ~= PATH_DELIMITER; + buf ~= s; + } + return convertPathDelimiters(buf); +} + +/** Split path into elements, e.g. /home/user/dir1 -> ["home", "user", "dir1"], "c:\dir1\dir2" -> ["c:", "dir1", "dir2"] */ +string[] splitPath(string path) { + string[] res; + int start = 0; + for (int i = 0; i <= path.length; i++) { + char ch = i < path.length ? path[i] : 0; + if (ch == '\\' || ch == '/' || ch == 0) { + if (start < i) + res ~= path[start .. i].dup; + start = i + 1; + } + } + return res; +} diff --git a/src/dlangui/dialogs/filedlg.d b/src/dlangui/dialogs/filedlg.d index 88475cef..2c46e0ac 100644 --- a/src/dlangui/dialogs/filedlg.d +++ b/src/dlangui/dialogs/filedlg.d @@ -48,23 +48,54 @@ enum FileDialogFlag : uint { Save = ConfirmOverwrite, } -/// file open / save dialog +/// File open / save dialog class FileDialog : Dialog { - EditLine path; - StringGridWidget list; - StringGridWidget places; - VerticalLayout leftPanel; - VerticalLayout rightPanel; + protected EditLine path; + protected EditLine filename; + protected StringGridWidget list; + //protected StringGridWidget places; + protected VerticalLayout leftPanel; + protected VerticalLayout rightPanel; + + protected RootEntry[] _roots; + this(UIString caption, Window parent, uint fileDialogFlags = DialogFlag.Modal | FileDialogFlag.FileMustExist) { super(caption, parent, fileDialogFlags); } + + protected void rootEntrySelected(RootEntry entry) { + // TODO + } + + protected Widget createRootsList() { + ListWidget list = new ListWidget("ROOTS_LIST"); + WidgetListAdapter adapter = new WidgetListAdapter(); + foreach(ref RootEntry root; _roots) { + ImageTextButton btn = new ImageTextButton(null, root.icon, root.label); + btn.orientation = Orientation.Vertical; + btn.styleId = "TRANSPARENT_BUTTON_BACKGROUND"; + btn.focusable = false; + btn.onClickListener = delegate(Widget source) { + rootEntrySelected(root); + return true; + }; + adapter.widgets.add(btn); + } + list.ownAdapter = adapter; + list.layoutWidth = WRAP_CONTENT; + list.layoutHeight = FILL_PARENT; + return list; + } + /// override to implement creation of dialog controls override void init() { + _roots = getRootPaths; layoutWidth(FILL_PARENT); layoutWidth(FILL_PARENT); LinearLayout content = new HorizontalLayout("dlgcontent"); content.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).minWidth(400).minHeight(300); leftPanel = new VerticalLayout("places"); + leftPanel.addChild(createRootsList()); rightPanel = new VerticalLayout("main"); leftPanel.layoutHeight(FILL_PARENT).minWidth(40); rightPanel.layoutHeight(FILL_PARENT).layoutWidth(FILL_PARENT); @@ -73,6 +104,8 @@ class FileDialog : Dialog { content.addChild(rightPanel); path = new EditLine("path"); path.layoutWidth(FILL_PARENT); + filename = new EditLine("path"); + filename.layoutWidth(FILL_PARENT); rightPanel.addChild(path); list = new StringGridWidget("files"); @@ -84,12 +117,13 @@ class FileDialog : Dialog { list.showRowHeaders = false; list.rowSelect = true; rightPanel.addChild(list); + rightPanel.addChild(filename); - places = new StringGridWidget("placesList"); - places.resize(1, 10); - places.showRowHeaders(false).showColHeaders(true); - places.setColTitle(0, "Places"d); - leftPanel.addChild(places); + //places = new StringGridWidget("placesList"); + //places.resize(1, 10); + //places.showRowHeaders(false).showColHeaders(true); + //places.setColTitle(0, "Places"d); + //leftPanel.addChild(places); addChild(content); addChild(createButtonsPanel([ACTION_OPEN, ACTION_CANCEL], 0, 0)); diff --git a/src/dlangui/widgets/controls.d b/src/dlangui/widgets/controls.d index 02679334..bec72a3e 100644 --- a/src/dlangui/widgets/controls.d +++ b/src/dlangui/widgets/controls.d @@ -207,14 +207,40 @@ class ImageButton : ImageWidget { class ImageTextButton : HorizontalLayout { protected ImageWidget _icon; protected TextWidget _label; + + /// Get label text override @property dstring text() { return _label.text; } + /// Set label plain unicode string override @property Widget text(dstring s) { _label.text = s; requestLayout(); return this; } + /// Set label string resource Id override @property Widget text(UIString s) { _label.text = s; requestLayout(); return this; } - this(string ID = null, string drawableId = null, string textResourceId = null) { - super(ID); + + /// Returns orientation: Vertical - image top, Horizontal - image left" + override @property Orientation orientation() { + return super.orientation(); + } + + /// Sets orientation: Vertical - image top, Horizontal - image left" + override @property LinearLayout orientation(Orientation value) { + if (!_icon || !_label) + return super.orientation(value); + if (value != orientation) { + super.orientation(value); + if (value == Orientation.Horizontal) { + _icon.alignment = Align.Left | Align.VCenter; + _label.alignment = Align.Right | Align.VCenter; + } else { + _icon.alignment = Align.Top | Align.HCenter; + _label.alignment = Align.Bottom | Align.HCenter; + } + } + return this; + } + + protected void init(string drawableId, UIString caption) { styleId = "BUTTON"; _icon = new ImageWidget("icon", drawableId); - _label = new TextWidget("label", textResourceId); + _label = new TextWidget("label", caption); _label.styleId = "BUTTON_LABEL"; _icon.state = State.Parent; _label.state = State.Parent; @@ -224,20 +250,17 @@ class ImageTextButton : HorizontalLayout { focusable = true; trackHover = true; } + + this(string ID = null, string drawableId = null, string textResourceId = null) { + super(ID); + UIString caption = textResourceId; + init(drawableId, caption); + } + this(string ID, string drawableId, dstring rawText) { super(ID); - styleId = "BUTTON"; - _icon = new ImageWidget("icon", drawableId); - _label = new TextWidget("label", rawText); - _label.styleId = "BUTTON_LABEL"; - _icon.styleId = "BUTTON_ICON"; - _icon.state = State.Parent; - _label.state = State.Parent; - addChild(_icon); - addChild(_label); - clickable = true; - focusable = true; - trackHover = true; + UIString caption = rawText; + init(drawableId, caption); } } diff --git a/src/dlangui/widgets/lists.d b/src/dlangui/widgets/lists.d index 69b29c05..d50b236d 100644 --- a/src/dlangui/widgets/lists.d +++ b/src/dlangui/widgets/lists.d @@ -38,7 +38,7 @@ interface ListAdapter { /// List adapter for simple list of widget instances class WidgetListAdapter : ListAdapter { - WidgetList _widgets; + private WidgetList _widgets; /// list of widgets to display @property ref WidgetList widgets() { return _widgets; } /// returns number of widgets in list