summaryrefslogtreecommitdiff
path: root/toolkit/components/filepicker
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/filepicker')
-rw-r--r--toolkit/components/filepicker/content/filepicker.js833
-rw-r--r--toolkit/components/filepicker/content/filepicker.xul80
-rw-r--r--toolkit/components/filepicker/jar.mn8
-rw-r--r--toolkit/components/filepicker/moz.build25
-rw-r--r--toolkit/components/filepicker/nsFilePicker.js319
-rw-r--r--toolkit/components/filepicker/nsFilePicker.manifest4
-rw-r--r--toolkit/components/filepicker/nsFileView.cpp982
-rw-r--r--toolkit/components/filepicker/nsIFileView.idl34
-rw-r--r--toolkit/components/filepicker/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/filepicker/test/unit/test_filecomplete.js45
-rw-r--r--toolkit/components/filepicker/test/unit/xpcshell.ini7
11 files changed, 2344 insertions, 0 deletions
diff --git a/toolkit/components/filepicker/content/filepicker.js b/toolkit/components/filepicker/content/filepicker.js
new file mode 100644
index 0000000000..6f91066ba5
--- /dev/null
+++ b/toolkit/components/filepicker/content/filepicker.js
@@ -0,0 +1,833 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const nsIFilePicker = Components.interfaces.nsIFilePicker;
+const nsIProperties = Components.interfaces.nsIProperties;
+const NS_DIRECTORYSERVICE_CONTRACTID = "@mozilla.org/file/directory_service;1";
+const NS_IOSERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
+const nsIFileView = Components.interfaces.nsIFileView;
+const NS_FILEVIEW_CONTRACTID = "@mozilla.org/filepicker/fileview;1";
+const nsITreeView = Components.interfaces.nsITreeView;
+const nsILocalFile = Components.interfaces.nsILocalFile;
+const nsIFile = Components.interfaces.nsIFile;
+const NS_LOCAL_FILE_CONTRACTID = "@mozilla.org/file/local;1";
+const NS_PROMPTSERVICE_CONTRACTID = "@mozilla.org/embedcomp/prompt-service;1";
+
+var sfile = Components.classes[NS_LOCAL_FILE_CONTRACTID].createInstance(nsILocalFile);
+var retvals;
+var filePickerMode;
+var homeDir;
+var treeView;
+var allowURLs;
+
+var textInput;
+var okButton;
+
+var gFilePickerBundle;
+
+// name of new directory entered by the user to be remembered
+// for next call of newDir() in case something goes wrong with creation
+var gNewDirName = { value: "" };
+
+function filepickerLoad() {
+ gFilePickerBundle = document.getElementById("bundle_filepicker");
+
+ textInput = document.getElementById("textInput");
+ okButton = document.documentElement.getButton("accept");
+ treeView = Components.classes[NS_FILEVIEW_CONTRACTID].createInstance(nsIFileView);
+
+ if (window.arguments) {
+ var o = window.arguments[0];
+ retvals = o.retvals; /* set this to a global var so we can set return values */
+ const title = o.title;
+ filePickerMode = o.mode;
+ if (o.displayDirectory) {
+ var directory = o.displayDirectory.path;
+ }
+
+ const initialText = o.defaultString;
+ var filterTitles = o.filters.titles;
+ var filterTypes = o.filters.types;
+ var numFilters = filterTitles.length;
+
+ document.title = title;
+ allowURLs = o.allowURLs;
+
+ if (initialText) {
+ textInput.value = initialText;
+ }
+ }
+
+ if (filePickerMode != nsIFilePicker.modeOpen && filePickerMode != nsIFilePicker.modeOpenMultiple) {
+ var newDirButton = document.getElementById("newDirButton");
+ newDirButton.removeAttribute("hidden");
+ }
+
+ if (filePickerMode == nsIFilePicker.modeGetFolder) {
+ var textInputLabel = document.getElementById("textInputLabel");
+ textInputLabel.value = gFilePickerBundle.getString("dirTextInputLabel");
+ textInputLabel.accessKey = gFilePickerBundle.getString("dirTextInputAccesskey");
+ }
+
+ if ((filePickerMode == nsIFilePicker.modeOpen) ||
+ (filePickerMode == nsIFilePicker.modeOpenMultiple) ||
+ (filePickerMode == nsIFilePicker.modeSave)) {
+
+ /* build filter popup */
+ var filterPopup = document.createElement("menupopup");
+
+ for (var i = 0; i < numFilters; i++) {
+ var menuItem = document.createElement("menuitem");
+ if (filterTypes[i] == "..apps")
+ menuItem.setAttribute("label", filterTitles[i]);
+ else
+ menuItem.setAttribute("label", filterTitles[i] + " (" + filterTypes[i] + ")");
+ menuItem.setAttribute("filters", filterTypes[i]);
+ filterPopup.appendChild(menuItem);
+ }
+
+ var filterMenuList = document.getElementById("filterMenuList");
+ filterMenuList.appendChild(filterPopup);
+ if (numFilters > 0)
+ filterMenuList.selectedIndex = 0;
+ var filterBox = document.getElementById("filterBox");
+ filterBox.removeAttribute("hidden");
+
+ filterMenuList.selectedIndex = o.filterIndex;
+
+ treeView.setFilter(filterTypes[o.filterIndex]);
+
+ } else if (filePickerMode == nsIFilePicker.modeGetFolder) {
+ treeView.showOnlyDirectories = true;
+ }
+
+ // The dialog defaults to an "open" icon, change it to "save" if applicable
+ if (filePickerMode == nsIFilePicker.modeSave)
+ okButton.setAttribute("icon", "save");
+
+ // start out with a filename sort
+ handleColumnClick("FilenameColumn");
+
+ try {
+ setOKAction();
+ } catch (exception) {
+ // keep it set to "OK"
+ }
+
+ // setup the dialogOverlay.xul button handlers
+ retvals.buttonStatus = nsIFilePicker.returnCancel;
+
+ var tree = document.getElementById("directoryTree");
+ if (filePickerMode == nsIFilePicker.modeOpenMultiple)
+ tree.removeAttribute("seltype");
+
+ tree.view = treeView;
+
+ // Start out with the ok button disabled since nothing will be
+ // selected and nothing will be in the text field.
+ okButton.disabled = filePickerMode != nsIFilePicker.modeGetFolder;
+
+ // This allows the window to show onscreen before we begin
+ // loading the file list
+
+ setTimeout(setInitialDirectory, 0, directory);
+}
+
+function setInitialDirectory(directory)
+{
+ // Start in the user's home directory
+ var dirService = Components.classes[NS_DIRECTORYSERVICE_CONTRACTID]
+ .getService(nsIProperties);
+ homeDir = dirService.get("Home", Components.interfaces.nsIFile);
+
+ if (directory) {
+ sfile.initWithPath(directory);
+ if (!sfile.exists() || !sfile.isDirectory())
+ directory = false;
+ }
+ if (!directory) {
+ sfile.initWithPath(homeDir.path);
+ }
+
+ gotoDirectory(sfile);
+}
+
+function onFilterChanged(target)
+{
+ // Do this on a timeout callback so the filter list can roll up
+ // and we don't keep the mouse grabbed while we are refiltering.
+
+ setTimeout(changeFilter, 0, target.getAttribute("filters"));
+}
+
+function changeFilter(filterTypes)
+{
+ window.setCursor("wait");
+ treeView.setFilter(filterTypes);
+ window.setCursor("auto");
+}
+
+function showErrorDialog(titleStrName, messageStrName, file)
+{
+ var errorTitle =
+ gFilePickerBundle.getFormattedString(titleStrName, [file.path]);
+ var errorMessage =
+ gFilePickerBundle.getFormattedString(messageStrName, [file.path]);
+ var promptService =
+ Components.classes[NS_PROMPTSERVICE_CONTRACTID].getService(Components.interfaces.nsIPromptService);
+
+ promptService.alert(window, errorTitle, errorMessage);
+}
+
+function openOnOK()
+{
+ var dir = treeView.selectedFiles.queryElementAt(0, nsIFile);
+ if (dir)
+ gotoDirectory(dir);
+
+ return false;
+}
+
+function selectOnOK()
+{
+ var errorTitle, errorMessage, promptService;
+ var ret = nsIFilePicker.returnOK;
+
+ var isDir = false;
+ var isFile = false;
+
+ retvals.filterIndex = document.getElementById("filterMenuList").selectedIndex;
+ retvals.fileURL = null;
+
+ if (allowURLs) {
+ try {
+ var ios = Components.classes[NS_IOSERVICE_CONTRACTID].getService(Components.interfaces.nsIIOService);
+ retvals.fileURL = ios.newURI(textInput.value, null, null);
+ let fileList = [];
+ if (retvals.fileURL instanceof Components.interfaces.nsIFileURL)
+ fileList.push(retvals.fileURL.file);
+ gFilesEnumerator.mFiles = fileList;
+ retvals.files = gFilesEnumerator;
+ retvals.buttonStatus = ret;
+
+ return true;
+ } catch (e) {
+ }
+ }
+
+ var fileList = processPath(textInput.value);
+ if (!fileList) {
+ // generic error message, should probably never happen
+ showErrorDialog("errorPathProblemTitle",
+ "errorPathProblemMessage",
+ textInput.value);
+ return false;
+ }
+
+ var curFileIndex;
+ for (curFileIndex = 0; curFileIndex < fileList.length &&
+ ret != nsIFilePicker.returnCancel; ++curFileIndex) {
+ var file = fileList[curFileIndex].QueryInterface(nsIFile);
+
+ // try to normalize - if this fails we will ignore the error
+ // because we will notice the
+ // error later and show a fitting error alert.
+ try {
+ file.normalize();
+ } catch (e) {
+ // promptService.alert(window, "Problem", "normalize failed, continuing");
+ }
+
+ var fileExists = file.exists();
+
+ if (!fileExists && (filePickerMode == nsIFilePicker.modeOpen ||
+ filePickerMode == nsIFilePicker.modeOpenMultiple)) {
+ showErrorDialog("errorOpenFileDoesntExistTitle",
+ "errorOpenFileDoesntExistMessage",
+ file);
+ return false;
+ }
+
+ if (!fileExists && filePickerMode == nsIFilePicker.modeGetFolder) {
+ showErrorDialog("errorDirDoesntExistTitle",
+ "errorDirDoesntExistMessage",
+ file);
+ return false;
+ }
+
+ if (fileExists) {
+ isDir = file.isDirectory();
+ isFile = file.isFile();
+ }
+
+ switch (filePickerMode) {
+ case nsIFilePicker.modeOpen:
+ case nsIFilePicker.modeOpenMultiple:
+ if (isFile) {
+ if (file.isReadable()) {
+ retvals.directory = file.parent.path;
+ } else {
+ showErrorDialog("errorOpeningFileTitle",
+ "openWithoutPermissionMessage_file",
+ file);
+ ret = nsIFilePicker.returnCancel;
+ }
+ } else if (isDir) {
+ if (!sfile.equals(file)) {
+ gotoDirectory(file);
+ }
+ textInput.value = "";
+ doEnabling();
+ ret = nsIFilePicker.returnCancel;
+ }
+ break;
+ case nsIFilePicker.modeSave:
+ if (isFile) { // can only be true if file.exists()
+ if (!file.isWritable()) {
+ showErrorDialog("errorSavingFileTitle",
+ "saveWithoutPermissionMessage_file",
+ file);
+ ret = nsIFilePicker.returnCancel;
+ } else {
+ // we need to pop up a dialog asking if you want to save
+ var confirmTitle = gFilePickerBundle.getString("confirmTitle");
+ var message =
+ gFilePickerBundle.getFormattedString("confirmFileReplacing",
+ [file.path]);
+
+ promptService = Components.classes[NS_PROMPTSERVICE_CONTRACTID].getService(Components.interfaces.nsIPromptService);
+ var rv = promptService.confirm(window, confirmTitle, message);
+ if (rv) {
+ ret = nsIFilePicker.returnReplace;
+ retvals.directory = file.parent.path;
+ } else {
+ ret = nsIFilePicker.returnCancel;
+ }
+ }
+ } else if (isDir) {
+ if (!sfile.equals(file)) {
+ gotoDirectory(file);
+ }
+ textInput.value = "";
+ doEnabling();
+ ret = nsIFilePicker.returnCancel;
+ } else {
+ var parent = file.parent;
+ if (parent.exists() && parent.isDirectory() && parent.isWritable()) {
+ retvals.directory = parent.path;
+ } else {
+ var oldParent = parent;
+ while (!parent.exists()) {
+ oldParent = parent;
+ parent = parent.parent;
+ }
+ errorTitle =
+ gFilePickerBundle.getFormattedString("errorSavingFileTitle",
+ [file.path]);
+ if (parent.isFile()) {
+ errorMessage =
+ gFilePickerBundle.getFormattedString("saveParentIsFileMessage",
+ [parent.path, file.path]);
+ } else {
+ errorMessage =
+ gFilePickerBundle.getFormattedString("saveParentDoesntExistMessage",
+ [oldParent.path, file.path]);
+ }
+ if (!parent.isWritable()) {
+ errorMessage =
+ gFilePickerBundle.getFormattedString("saveWithoutPermissionMessage_dir", [parent.path]);
+ }
+ promptService = Components.classes[NS_PROMPTSERVICE_CONTRACTID].getService(Components.interfaces.nsIPromptService);
+ promptService.alert(window, errorTitle, errorMessage);
+ ret = nsIFilePicker.returnCancel;
+ }
+ }
+ break;
+ case nsIFilePicker.modeGetFolder:
+ if (isDir) {
+ retvals.directory = file.parent.path;
+ } else { // if nothing selected, the current directory will be fine
+ retvals.directory = sfile.path;
+ }
+ break;
+ }
+ }
+
+ gFilesEnumerator.mFiles = fileList;
+
+ retvals.files = gFilesEnumerator;
+ retvals.buttonStatus = ret;
+
+ return (ret != nsIFilePicker.returnCancel);
+}
+
+var gFilesEnumerator = {
+ mFiles: null,
+ mIndex: 0,
+
+ hasMoreElements: function()
+ {
+ return (this.mIndex < this.mFiles.length);
+ },
+ getNext: function()
+ {
+ if (this.mIndex >= this.mFiles.length)
+ throw Components.results.NS_ERROR_FAILURE;
+ return this.mFiles[this.mIndex++];
+ }
+};
+
+function onCancel()
+{
+ // Close the window.
+ retvals.buttonStatus = nsIFilePicker.returnCancel;
+ retvals.file = null;
+ retvals.files = null;
+ return true;
+}
+
+function onDblClick(e) {
+ // we only care about button 0 (left click) events
+ if (e.button != 0) return;
+
+ var t = e.originalTarget;
+ if (t.localName != "treechildren")
+ return;
+
+ openSelectedFile();
+}
+
+function openSelectedFile() {
+ var fileList = treeView.selectedFiles;
+ if (fileList.length == 0)
+ return;
+
+ var file = fileList.queryElementAt(0, nsIFile);
+ if (file.isDirectory())
+ gotoDirectory(file);
+ else if (file.isFile())
+ document.documentElement.acceptDialog();
+}
+
+function onClick(e) {
+ var t = e.originalTarget;
+ if (t.localName == "treecol")
+ handleColumnClick(t.id);
+}
+
+function convertColumnIDtoSortType(columnID) {
+ var sortKey;
+
+ switch (columnID) {
+ case "FilenameColumn":
+ sortKey = nsIFileView.sortName;
+ break;
+ case "FileSizeColumn":
+ sortKey = nsIFileView.sortSize;
+ break;
+ case "LastModifiedColumn":
+ sortKey = nsIFileView.sortDate;
+ break;
+ default:
+ dump("unsupported sort column: " + columnID + "\n");
+ sortKey = 0;
+ break;
+ }
+
+ return sortKey;
+}
+
+function handleColumnClick(columnID) {
+ var sortType = convertColumnIDtoSortType(columnID);
+ var sortOrder = (treeView.sortType == sortType) ? !treeView.reverseSort : false;
+ treeView.sort(sortType, sortOrder);
+
+ // set the sort indicator on the column we are sorted by
+ var sortedColumn = document.getElementById(columnID);
+ if (treeView.reverseSort) {
+ sortedColumn.setAttribute("sortDirection", "descending");
+ } else {
+ sortedColumn.setAttribute("sortDirection", "ascending");
+ }
+
+ // remove the sort indicator from the rest of the columns
+ var currCol = sortedColumn.parentNode.firstChild;
+ while (currCol) {
+ if (currCol != sortedColumn && currCol.localName == "treecol")
+ currCol.removeAttribute("sortDirection");
+ currCol = currCol.nextSibling;
+ }
+}
+
+function onKeypress(e) {
+ if (e.keyCode == 8) /* backspace */
+ goUp();
+
+ /* enter is handled by the ondialogaccept handler */
+}
+
+function doEnabling() {
+ if (filePickerMode != nsIFilePicker.modeGetFolder)
+ // Maybe add check if textInput.value would resolve to an existing
+ // file or directory in .modeOpen. Too costly I think.
+ okButton.disabled = (textInput.value == "")
+}
+
+function onTreeFocus(event) {
+ // Reset the button label and enabled/disabled state.
+ onFileSelected(treeView.selectedFiles);
+}
+
+function setOKAction(file) {
+ var buttonLabel;
+ var buttonIcon = "open"; // used in all but one case
+
+ if (file && file.isDirectory()) {
+ document.documentElement.setAttribute("ondialogaccept", "return openOnOK();");
+ buttonLabel = gFilePickerBundle.getString("openButtonLabel");
+ }
+ else {
+ document.documentElement.setAttribute("ondialogaccept", "return selectOnOK();");
+ switch (filePickerMode) {
+ case nsIFilePicker.modeGetFolder:
+ buttonLabel = gFilePickerBundle.getString("selectFolderButtonLabel");
+ break;
+ case nsIFilePicker.modeOpen:
+ case nsIFilePicker.modeOpenMultiple:
+ buttonLabel = gFilePickerBundle.getString("openButtonLabel");
+ break;
+ case nsIFilePicker.modeSave:
+ buttonLabel = gFilePickerBundle.getString("saveButtonLabel");
+ buttonIcon = "save";
+ break;
+ }
+ }
+ okButton.setAttribute("label", buttonLabel);
+ okButton.setAttribute("icon", buttonIcon);
+}
+
+function onSelect(event) {
+ onFileSelected(treeView.selectedFiles);
+}
+
+function onFileSelected(/* nsIArray */ selectedFileList) {
+ var validFileSelected = false;
+ var invalidSelection = false;
+ var file;
+ var fileCount = selectedFileList.length;
+
+ for (var index = 0; index < fileCount; ++index) {
+ file = selectedFileList.queryElementAt(index, nsIFile);
+ if (file) {
+ var path = file.leafName;
+
+ if (path) {
+ var isDir = file.isDirectory();
+ if ((filePickerMode == nsIFilePicker.modeGetFolder) || !isDir) {
+ if (!validFileSelected)
+ textInput.value = "";
+ addToTextFieldValue(path);
+ }
+
+ if (isDir && fileCount > 1) {
+ // The user has selected multiple items, and one of them is
+ // a directory. This is not a valid state, so we'll disable
+ // the ok button.
+ invalidSelection = true;
+ }
+
+ validFileSelected = true;
+ }
+ }
+ }
+
+ if (validFileSelected) {
+ setOKAction(file);
+ okButton.disabled = invalidSelection;
+ } else if (filePickerMode != nsIFilePicker.modeGetFolder)
+ okButton.disabled = (textInput.value == "");
+}
+
+function addToTextFieldValue(path)
+{
+ var newValue = "";
+
+ if (textInput.value == "")
+ newValue = path.replace(/\"/g, "\\\"");
+ else {
+ // Quote the existing text if needed,
+ // then append the new filename (quoted and escaped)
+ if (textInput.value[0] != '"')
+ newValue = '"' + textInput.value.replace(/\"/g, "\\\"") + '"';
+ else
+ newValue = textInput.value;
+
+ newValue = newValue + ' "' + path.replace(/\"/g, "\\\"") + '"';
+ }
+
+ textInput.value = newValue;
+}
+
+function onTextFieldFocus() {
+ setOKAction(null);
+ doEnabling();
+}
+
+function onDirectoryChanged(target)
+{
+ var path = target.getAttribute("label");
+
+ var file = Components.classes[NS_LOCAL_FILE_CONTRACTID].createInstance(nsILocalFile);
+ file.initWithPath(path);
+
+ if (!sfile.equals(file)) {
+ // Do this on a timeout callback so the directory list can roll up
+ // and we don't keep the mouse grabbed while we are loading.
+
+ setTimeout(gotoDirectory, 0, file);
+ }
+}
+
+function populateAncestorList(directory) {
+ var menu = document.getElementById("lookInMenu");
+
+ while (menu.hasChildNodes()) {
+ menu.removeChild(menu.firstChild);
+ }
+
+ var menuItem = document.createElement("menuitem");
+ menuItem.setAttribute("label", directory.path);
+ menuItem.setAttribute("crop", "start");
+ menu.appendChild(menuItem);
+
+ // .parent is _sometimes_ null, see bug 121489. Do a dance around that.
+ var parent = directory.parent;
+ while (parent && !parent.equals(directory)) {
+ menuItem = document.createElement("menuitem");
+ menuItem.setAttribute("label", parent.path);
+ menuItem.setAttribute("crop", "start");
+ menu.appendChild(menuItem);
+ directory = parent;
+ parent = directory.parent;
+ }
+
+ var menuList = document.getElementById("lookInMenuList");
+ menuList.selectedIndex = 0;
+}
+
+function goUp() {
+ try {
+ var parent = sfile.parent;
+ } catch (ex) { dump("can't get parent directory\n"); }
+
+ if (parent) {
+ gotoDirectory(parent);
+ }
+}
+
+function goHome() {
+ gotoDirectory(homeDir);
+}
+
+function newDir() {
+ var file;
+ var promptService =
+ Components.classes[NS_PROMPTSERVICE_CONTRACTID].getService(Components.interfaces.nsIPromptService);
+ var dialogTitle =
+ gFilePickerBundle.getString("promptNewDirTitle");
+ var dialogMsg =
+ gFilePickerBundle.getString("promptNewDirMessage");
+ var ret = promptService.prompt(window, dialogTitle, dialogMsg, gNewDirName, null, {value:0});
+
+ if (ret) {
+ file = processPath(gNewDirName.value);
+ if (!file) {
+ showErrorDialog("errorCreateNewDirTitle",
+ "errorCreateNewDirMessage",
+ file);
+ return false;
+ }
+
+ file = file[0].QueryInterface(nsIFile);
+ if (file.exists()) {
+ showErrorDialog("errorNewDirDoesExistTitle",
+ "errorNewDirDoesExistMessage",
+ file);
+ return false;
+ }
+
+ var parent = file.parent;
+ if (!(parent.exists() && parent.isDirectory() && parent.isWritable())) {
+ while (!parent.exists()) {
+ parent = parent.parent;
+ }
+ if (parent.isFile()) {
+ showErrorDialog("errorCreateNewDirTitle",
+ "errorCreateNewDirIsFileMessage",
+ parent);
+ return false;
+ }
+ if (!parent.isWritable()) {
+ showErrorDialog("errorCreateNewDirTitle",
+ "errorCreateNewDirPermissionMessage",
+ parent);
+ return false;
+ }
+ }
+
+ try {
+ file.create(nsIFile.DIRECTORY_TYPE, 0o755);
+ } catch (e) {
+ showErrorDialog("errorCreateNewDirTitle",
+ "errorCreateNewDirMessage",
+ file);
+ return false;
+ }
+ file.normalize(); // ... in case ".." was used in the path
+ gotoDirectory(file);
+ // we remember and reshow a dirname if something goes wrong
+ // so that errors can be corrected more easily. If all went well,
+ // reset the default value to blank
+ gNewDirName = { value: "" };
+ }
+ return true;
+}
+
+function gotoDirectory(directory) {
+ window.setCursor("wait");
+ try {
+ populateAncestorList(directory);
+ treeView.setDirectory(directory);
+ document.getElementById("errorShower").selectedIndex = 0;
+ } catch (ex) {
+ document.getElementById("errorShower").selectedIndex = 1;
+ }
+
+ window.setCursor("auto");
+
+ if (filePickerMode == nsIFilePicker.modeGetFolder) {
+ textInput.value = "";
+ }
+ textInput.focus();
+ textInput.setAttribute("autocompletesearchparam", directory.path);
+ sfile = directory;
+}
+
+function toggleShowHidden(event) {
+ treeView.showHiddenFiles = !treeView.showHiddenFiles;
+}
+
+// from the current directory and whatever was entered
+// in the entry field, try to make a new path. This
+// uses "/" as the directory separator, "~" as a shortcut
+// for the home directory (but only when seen at the start
+// of a path), and ".." to denote the parent directory.
+// returns an array of the files listed,
+// or false if an error occurred.
+function processPath(path)
+{
+ var fileArray = new Array();
+ var strLength = path.length;
+
+ if (path[0] == '"' && filePickerMode == nsIFilePicker.modeOpenMultiple &&
+ strLength > 1) {
+ // we have a quoted list of filenames, separated by spaces.
+ // iterate the list and process each file.
+
+ var curFileStart = 1;
+
+ while (1) {
+ var nextQuote;
+
+ // Look for an unescaped quote
+ var quoteSearchStart = curFileStart + 1;
+ do {
+ nextQuote = path.indexOf('"', quoteSearchStart);
+ quoteSearchStart = nextQuote + 1;
+ } while (nextQuote != -1 && path[nextQuote - 1] == '\\');
+
+ if (nextQuote == -1) {
+ // we have a filename with no trailing quote.
+ // just assume that the filename ends at the end of the string.
+
+ if (!processPathEntry(path.substring(curFileStart), fileArray))
+ return false;
+ break;
+ }
+
+ if (!processPathEntry(path.substring(curFileStart, nextQuote), fileArray))
+ return false;
+
+ curFileStart = path.indexOf('"', nextQuote + 1);
+ if (curFileStart == -1) {
+ // no more quotes, but if we're not at the end of the string,
+ // go ahead and process the remaining text.
+
+ if (nextQuote < strLength - 1)
+ if (!processPathEntry(path.substring(nextQuote + 1), fileArray))
+ return false;
+ break;
+ }
+ ++curFileStart;
+ }
+ } else if (!processPathEntry(path, fileArray)) {
+ // If we didn't start with a quote, assume we just have a single file.
+ return false;
+ }
+
+ return fileArray;
+}
+
+function processPathEntry(path, fileArray)
+{
+ var filePath;
+ var file;
+
+ try {
+ file = sfile.clone().QueryInterface(nsILocalFile);
+ } catch (e) {
+ dump("Couldn't clone\n"+e);
+ return false;
+ }
+
+ var tilde_file = file.clone();
+ tilde_file.append("~");
+ if (path[0] == '~' && // Expand ~ to $HOME, except:
+ !(path == "~" && tilde_file.exists()) && // If ~ was entered and such a file exists, don't expand
+ (path.length == 1 || path[1] == "/")) // We don't want to expand ~file to ${HOME}file
+ filePath = homeDir.path + path.substring(1);
+ else
+ filePath = path;
+
+ // Unescape quotes
+ filePath = filePath.replace(/\\\"/g, "\"");
+
+ if (filePath[0] == '/') /* an absolute path was entered */
+ file.initWithPath(filePath);
+ else if ((filePath.indexOf("/../") > 0) ||
+ (filePath.substr(-3) == "/..") ||
+ (filePath.substr(0, 3) == "../") ||
+ (filePath == "..")) {
+ /* appendRelativePath doesn't allow .. */
+ try {
+ file.initWithPath(file.path + "/" + filePath);
+ } catch (e) {
+ dump("Couldn't init path\n"+e);
+ return false;
+ }
+ }
+ else {
+ try {
+ file.appendRelativePath(filePath);
+ } catch (e) {
+ dump("Couldn't append path\n"+e);
+ return false;
+ }
+ }
+
+ fileArray[fileArray.length] = file;
+ return true;
+}
diff --git a/toolkit/components/filepicker/content/filepicker.xul b/toolkit/components/filepicker/content/filepicker.xul
new file mode 100644
index 0000000000..4bf3112307
--- /dev/null
+++ b/toolkit/components/filepicker/content/filepicker.xul
@@ -0,0 +1,80 @@
+<?xml version="1.0"?> <!-- -*- Mode: HTML -*- -->
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/filepicker.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://global/locale/filepicker.dtd" >
+
+<dialog id="main-window"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="filepickerLoad();"
+ width="426" height="300"
+ ondialogaccept="return selectOnOK();"
+ ondialogcancel="return onCancel();"
+ persist="screenX screenY width height">
+
+<stringbundle id="bundle_filepicker" src="chrome://global/locale/filepicker.properties"/>
+<script type="application/javascript" src="chrome://global/content/filepicker.js"/>
+
+<hbox align="center">
+ <label value="&lookInMenuList.label;" control="lookInMenuList" accesskey="&lookInMenuList.accesskey;"/>
+ <menulist id="lookInMenuList" flex="1" oncommand="onDirectoryChanged(event.target);" crop="start">
+ <menupopup id="lookInMenu"/>
+ </menulist>
+ <button id="folderUpButton" class="up-button" tooltiptext="&folderUp.tooltiptext;" oncommand="goUp();"/>
+ <button id="homeButton" class="home-button" tooltiptext="&folderHome.tooltiptext;" oncommand="goHome();"/>
+ <button id="newDirButton" hidden="true" class="new-dir-button" tooltiptext="&folderNew.tooltiptext;" oncommand="newDir();"/>
+</hbox>
+
+<hbox flex="1">
+ <deck id="errorShower" flex="1">
+ <tree id="directoryTree" flex="1" class="focusring" seltype="single"
+ onclick="onClick(event);"
+ ondblclick="onDblClick(event);"
+ onkeypress="onKeypress(event);"
+ onfocus="onTreeFocus(event);"
+ onselect="onSelect(event);">
+ <treecols>
+ <treecol id="FilenameColumn" label="&name.label;" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="FileSizeColumn" label="&size.label;" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="LastModifiedColumn" label="&lastModified.label;" flex="1"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <label>&noPermissionError.label;</label>
+ </deck>
+</hbox>
+
+<grid style="margin-top: 5px">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <row align="center">
+ <label value="&textInput.label;" id="textInputLabel" control="textInput" accesskey="&textInput.accesskey;"/>
+ <textbox id="textInput" flex="1" oninput="doEnabling()"
+ type="autocomplete" autocompletesearch="file"
+ onfocus="onTextFieldFocus();"/>
+ </row>
+ <row id="filterBox" hidden="true" align="center">
+ <label value="&filterMenuList.label;" control="filterMenuList" accesskey="&filterMenuList.accesskey;"/>
+ <menulist id="filterMenuList" flex="1" oncommand="onFilterChanged(event.target);"/>
+ </row>
+ </rows>
+</grid>
+<hbox class="dialog-button-box" align="center">
+ <checkbox label="&showHiddenFiles.label;" oncommand="toggleShowHidden();"
+ flex="1" accesskey="&showHiddenFiles.accesskey;"/>
+ <button dlgtype="cancel" icon="cancel" class="dialog-button"/>
+ <button dlgtype="accept" icon="open" class="dialog-button"/>
+</hbox>
+</dialog>
diff --git a/toolkit/components/filepicker/jar.mn b/toolkit/components/filepicker/jar.mn
new file mode 100644
index 0000000000..ba585f3a41
--- /dev/null
+++ b/toolkit/components/filepicker/jar.mn
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+ content/global/filepicker.xul (content/filepicker.xul)
+ content/global/filepicker.js (content/filepicker.js)
+
diff --git a/toolkit/components/filepicker/moz.build b/toolkit/components/filepicker/moz.build
new file mode 100644
index 0000000000..0990cb00fd
--- /dev/null
+++ b/toolkit/components/filepicker/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_MODULE = 'filepicker'
+XPIDL_SOURCES += [
+ 'nsIFileView.idl',
+]
+SOURCES += [
+ 'nsFileView.cpp',
+]
+EXTRA_COMPONENTS += [
+ 'nsFilePicker.js',
+]
+EXTRA_PP_COMPONENTS += [
+ 'nsFilePicker.manifest',
+]
+XPCSHELL_TESTS_MANIFESTS += [
+ 'test/unit/xpcshell.ini',
+]
+FINAL_LIBRARY = 'xul'
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/toolkit/components/filepicker/nsFilePicker.js b/toolkit/components/filepicker/nsFilePicker.js
new file mode 100644
index 0000000000..8c92ff821b
--- /dev/null
+++ b/toolkit/components/filepicker/nsFilePicker.js
@@ -0,0 +1,319 @@
+/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * No magic constructor behaviour, as is de rigeur for XPCOM.
+ * If you must perform some initialization, and it could possibly fail (even
+ * due to an out-of-memory condition), you should use an Init method, which
+ * can convey failure appropriately (thrown exception in JS,
+ * NS_FAILED(nsresult) return in C++).
+ *
+ * In JS, you can actually cheat, because a thrown exception will cause the
+ * CreateInstance call to fail in turn, but not all languages are so lucky.
+ * (Though ANSI C++ provides exceptions, they are verboten in Mozilla code
+ * for portability reasons -- and even when you're building completely
+ * platform-specific code, you can't throw across an XPCOM method boundary.)
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const DEBUG = false; /* set to true to enable debug messages */
+
+const LOCAL_FILE_CONTRACTID = "@mozilla.org/file/local;1";
+const APPSHELL_SERV_CONTRACTID = "@mozilla.org/appshell/appShellService;1";
+const STRBUNDLE_SERV_CONTRACTID = "@mozilla.org/intl/stringbundle;1";
+
+const nsIAppShellService = Components.interfaces.nsIAppShellService;
+const nsILocalFile = Components.interfaces.nsILocalFile;
+const nsIFileURL = Components.interfaces.nsIFileURL;
+const nsISupports = Components.interfaces.nsISupports;
+const nsIFactory = Components.interfaces.nsIFactory;
+const nsIFilePicker = Components.interfaces.nsIFilePicker;
+const nsIInterfaceRequestor = Components.interfaces.nsIInterfaceRequestor;
+const nsIDOMWindow = Components.interfaces.nsIDOMWindow;
+const nsIStringBundleService = Components.interfaces.nsIStringBundleService;
+const nsIWebNavigation = Components.interfaces.nsIWebNavigation;
+const nsIDocShellTreeItem = Components.interfaces.nsIDocShellTreeItem;
+const nsIBaseWindow = Components.interfaces.nsIBaseWindow;
+
+var titleBundle = null;
+var filterBundle = null;
+var lastDirectory = null;
+
+function nsFilePicker()
+{
+ if (!titleBundle)
+ titleBundle = srGetStrBundle("chrome://global/locale/filepicker.properties");
+ if (!filterBundle)
+ filterBundle = srGetStrBundle("chrome://global/content/filepicker.properties");
+
+ /* attributes */
+ this.mDefaultString = "";
+ this.mFilterIndex = 0;
+ this.mFilterTitles = new Array();
+ this.mFilters = new Array();
+ this.mDisplayDirectory = null;
+ if (lastDirectory) {
+ try {
+ var dir = Components.classes[LOCAL_FILE_CONTRACTID].createInstance(nsILocalFile);
+ dir.initWithPath(lastDirectory);
+ this.mDisplayDirectory = dir;
+ } catch (e) {}
+ }
+}
+
+nsFilePicker.prototype = {
+ classID: Components.ID("{54ae32f8-1dd2-11b2-a209-df7c505370f8}"),
+
+ QueryInterface: function(iid) {
+ if (iid.equals(nsIFilePicker) ||
+ iid.equals(nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+
+ /* attribute nsILocalFile displayDirectory; */
+ set displayDirectory(a) {
+ this.mDisplayDirectory = a &&
+ a.clone().QueryInterface(nsILocalFile);
+ },
+ get displayDirectory() {
+ return this.mDisplayDirectory &&
+ this.mDisplayDirectory.clone()
+ .QueryInterface(nsILocalFile);
+ },
+
+ /* readonly attribute nsILocalFile file; */
+ get file() { return this.mFilesEnumerator.mFiles[0]; },
+
+ /* readonly attribute nsISimpleEnumerator files; */
+ get files() { return this.mFilesEnumerator; },
+
+ /* we don't support directories, yet */
+ get domFileOrDirectory() {
+ let enumerator = this.domFileOrDirectoryEnumerator;
+ return enumerator ? enumerator.mFiles[0] : null;
+ },
+
+ /* readonly attribute nsISimpleEnumerator domFileOrDirectoryEnumerator; */
+ get domFileOrDirectoryEnumerator() {
+ if (!this.mFilesEnumerator) {
+ return null;
+ }
+
+ if (!this.mDOMFilesEnumerator) {
+ this.mDOMFilesEnumerator = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISimpleEnumerator]),
+
+ mFiles: [],
+ mIndex: 0,
+
+ hasMoreElements: function() {
+ return (this.mIndex < this.mFiles.length);
+ },
+
+ getNext: function() {
+ if (this.mIndex >= this.mFiles.length) {
+ throw Components.results.NS_ERROR_FAILURE;
+ }
+ return this.mFiles[this.mIndex++];
+ }
+ };
+
+ var utils = this.mParentWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+ for (var i = 0; i < this.mFilesEnumerator.mFiles.length; ++i) {
+ var file = utils.wrapDOMFile(this.mFilesEnumerator.mFiles[i]);
+ this.mDOMFilesEnumerator.mFiles.push(file);
+ }
+ }
+
+ return this.mDOMFilesEnumerator;
+ },
+
+ /* readonly attribute nsIURI fileURL; */
+ get fileURL() {
+ if (this.mFileURL)
+ return this.mFileURL;
+
+ if (!this.mFilesEnumerator)
+ return null;
+
+ var ioService = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+
+ return this.mFileURL = ioService.newFileURI(this.file);
+ },
+
+ /* attribute wstring defaultString; */
+ set defaultString(a) { this.mDefaultString = a; },
+ get defaultString() { return this.mDefaultString; },
+
+ /* attribute wstring defaultExtension */
+ set defaultExtension(ext) { },
+ get defaultExtension() { return ""; },
+
+ /* attribute long filterIndex; */
+ set filterIndex(a) { this.mFilterIndex = a; },
+ get filterIndex() { return this.mFilterIndex; },
+
+ /* attribute boolean addToRecentDocs; */
+ set addToRecentDocs(a) {},
+ get addToRecentDocs() { return false; },
+
+ /* readonly attribute short mode; */
+ get mode() { return this.mMode; },
+
+ /* members */
+ mFilesEnumerator: undefined,
+ mDOMFilesEnumerator: undefined,
+ mParentWindow: null,
+
+ /* methods */
+ init: function(parent, title, mode) {
+ this.mParentWindow = parent;
+ this.mTitle = title;
+ this.mMode = mode;
+ },
+
+ appendFilters: function(filterMask) {
+ if (filterMask & nsIFilePicker.filterHTML) {
+ this.appendFilter(titleBundle.GetStringFromName("htmlTitle"),
+ filterBundle.GetStringFromName("htmlFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterText) {
+ this.appendFilter(titleBundle.GetStringFromName("textTitle"),
+ filterBundle.GetStringFromName("textFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterImages) {
+ this.appendFilter(titleBundle.GetStringFromName("imageTitle"),
+ filterBundle.GetStringFromName("imageFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterXML) {
+ this.appendFilter(titleBundle.GetStringFromName("xmlTitle"),
+ filterBundle.GetStringFromName("xmlFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterXUL) {
+ this.appendFilter(titleBundle.GetStringFromName("xulTitle"),
+ filterBundle.GetStringFromName("xulFilter"));
+ }
+ this.mAllowURLs = !!(filterMask & nsIFilePicker.filterAllowURLs);
+ if (filterMask & nsIFilePicker.filterApps) {
+ // We use "..apps" as a special filter for executable files
+ this.appendFilter(titleBundle.GetStringFromName("appsTitle"),
+ "..apps");
+ }
+ if (filterMask & nsIFilePicker.filterAudio) {
+ this.appendFilter(titleBundle.GetStringFromName("audioTitle"),
+ filterBundle.GetStringFromName("audioFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterVideo) {
+ this.appendFilter(titleBundle.GetStringFromName("videoTitle"),
+ filterBundle.GetStringFromName("videoFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterAll) {
+ this.appendFilter(titleBundle.GetStringFromName("allTitle"),
+ filterBundle.GetStringFromName("allFilter"));
+ }
+ },
+
+ appendFilter: function(title, extensions) {
+ this.mFilterTitles.push(title);
+ this.mFilters.push(extensions);
+ },
+
+ open: function(aFilePickerShownCallback) {
+ var tm = Components.classes["@mozilla.org/thread-manager;1"]
+ .getService(Components.interfaces.nsIThreadManager);
+ tm.mainThread.dispatch(function() {
+ let result = Components.interfaces.nsIFilePicker.returnCancel;
+ try {
+ result = this.show();
+ } catch (ex) {
+ }
+ if (aFilePickerShownCallback) {
+ aFilePickerShownCallback.done(result);
+ }
+ }.bind(this), Components.interfaces.nsIThread.DISPATCH_NORMAL);
+ },
+
+ show: function() {
+ var o = {};
+ o.title = this.mTitle;
+ o.mode = this.mMode;
+ o.displayDirectory = this.mDisplayDirectory;
+ o.defaultString = this.mDefaultString;
+ o.filterIndex = this.mFilterIndex;
+ o.filters = {};
+ o.filters.titles = this.mFilterTitles;
+ o.filters.types = this.mFilters;
+ o.allowURLs = this.mAllowURLs;
+ o.retvals = {};
+
+ var parent;
+ if (this.mParentWindow) {
+ parent = this.mParentWindow;
+ } else if (typeof(window) == "object" && window != null) {
+ parent = window;
+ } else {
+ try {
+ var appShellService = Components.classes[APPSHELL_SERV_CONTRACTID].getService(nsIAppShellService);
+ parent = appShellService.hiddenDOMWindow;
+ } catch (ex) {
+ debug("Can't get parent. xpconnect hates me so we can't get one from the appShellService.\n");
+ debug(ex + "\n");
+ }
+ }
+
+ try {
+ parent.openDialog("chrome://global/content/filepicker.xul",
+ "",
+ "chrome,modal,titlebar,resizable=yes,dependent=yes",
+ o);
+
+ this.mFilterIndex = o.retvals.filterIndex;
+ this.mFilesEnumerator = o.retvals.files;
+ this.mFileURL = o.retvals.fileURL;
+ lastDirectory = o.retvals.directory;
+ return o.retvals.buttonStatus;
+ } catch (ex) { dump("unable to open file picker\n" + ex + "\n"); }
+
+ return null;
+ }
+}
+
+if (DEBUG)
+ debug = function (s) { dump("-*- filepicker: " + s + "\n"); };
+else
+ debug = function (s) {};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsFilePicker]);
+
+/* crap from strres.js that I want to use for string bundles since I can't include another .js file.... */
+
+var strBundleService = null;
+
+function srGetStrBundle(path)
+{
+ var strBundle = null;
+
+ if (!strBundleService) {
+ try {
+ strBundleService = Components.classes[STRBUNDLE_SERV_CONTRACTID].getService(nsIStringBundleService);
+ } catch (ex) {
+ dump("\n--** strBundleService createInstance failed **--\n");
+ return null;
+ }
+ }
+
+ strBundle = strBundleService.createBundle(path);
+ if (!strBundle) {
+ dump("\n--** strBundle createInstance failed **--\n");
+ }
+ return strBundle;
+}
diff --git a/toolkit/components/filepicker/nsFilePicker.manifest b/toolkit/components/filepicker/nsFilePicker.manifest
new file mode 100644
index 0000000000..06c2e8956f
--- /dev/null
+++ b/toolkit/components/filepicker/nsFilePicker.manifest
@@ -0,0 +1,4 @@
+component {54ae32f8-1dd2-11b2-a209-df7c505370f8} nsFilePicker.js
+#ifndef MOZ_WIDGET_GTK
+contract @mozilla.org/filepicker;1 {54ae32f8-1dd2-11b2-a209-df7c505370f8}
+#endif
diff --git a/toolkit/components/filepicker/nsFileView.cpp b/toolkit/components/filepicker/nsFileView.cpp
new file mode 100644
index 0000000000..ad4471e862
--- /dev/null
+++ b/toolkit/components/filepicker/nsFileView.cpp
@@ -0,0 +1,982 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIFileView.h"
+#include "nsITreeView.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsITreeSelection.h"
+#include "nsITreeColumns.h"
+#include "nsITreeBoxObject.h"
+#include "nsIFile.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsCRT.h"
+#include "nsPrintfCString.h"
+#include "nsIDateTimeFormat.h"
+#include "nsQuickSort.h"
+#include "nsIAtom.h"
+#include "nsIAutoCompleteResult.h"
+#include "nsIAutoCompleteSearch.h"
+#include "nsISimpleEnumerator.h"
+#include "nsAutoPtr.h"
+#include "nsIMutableArray.h"
+#include "nsTArray.h"
+#include "mozilla/Attributes.h"
+
+#include "nsWildCard.h"
+
+class nsIDOMDataTransfer;
+
+#define NS_FILECOMPLETE_CID { 0xcb60980e, 0x18a5, 0x4a77, \
+ { 0x91, 0x10, 0x81, 0x46, 0x61, 0x4c, 0xa7, 0xf0 } }
+#define NS_FILECOMPLETE_CONTRACTID "@mozilla.org/autocomplete/search;1?name=file"
+
+class nsFileResult final : public nsIAutoCompleteResult
+{
+public:
+ // aSearchString is the text typed into the autocomplete widget
+ // aSearchParam is the picker's currently displayed directory
+ nsFileResult(const nsAString& aSearchString, const nsAString& aSearchParam);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAUTOCOMPLETERESULT
+
+ nsTArray<nsString> mValues;
+ nsString mSearchString;
+ uint16_t mSearchResult;
+private:
+ ~nsFileResult() {}
+};
+
+NS_IMPL_ISUPPORTS(nsFileResult, nsIAutoCompleteResult)
+
+nsFileResult::nsFileResult(const nsAString& aSearchString,
+ const nsAString& aSearchParam):
+ mSearchString(aSearchString)
+{
+ if (aSearchString.IsEmpty())
+ mSearchResult = RESULT_IGNORED;
+ else {
+ int32_t slashPos = mSearchString.RFindChar('/');
+ mSearchResult = RESULT_FAILURE;
+ nsCOMPtr<nsIFile> directory;
+ nsDependentSubstring parent(Substring(mSearchString, 0, slashPos + 1));
+ if (!parent.IsEmpty() && parent.First() == '/')
+ NS_NewLocalFile(parent, true, getter_AddRefs(directory));
+ if (!directory) {
+ if (NS_FAILED(NS_NewLocalFile(aSearchParam, true, getter_AddRefs(directory))))
+ return;
+ if (slashPos > 0)
+ directory->AppendRelativePath(Substring(mSearchString, 0, slashPos));
+ }
+ nsCOMPtr<nsISimpleEnumerator> dirEntries;
+ if (NS_FAILED(directory->GetDirectoryEntries(getter_AddRefs(dirEntries))))
+ return;
+ mSearchResult = RESULT_NOMATCH;
+ bool hasMore = false;
+ nsDependentSubstring prefix(Substring(mSearchString, slashPos + 1));
+ while (NS_SUCCEEDED(dirEntries->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> nextItem;
+ dirEntries->GetNext(getter_AddRefs(nextItem));
+ nsCOMPtr<nsIFile> nextFile(do_QueryInterface(nextItem));
+ nsAutoString fileName;
+ nextFile->GetLeafName(fileName);
+ if (StringBeginsWith(fileName, prefix)) {
+ fileName.Insert(parent, 0);
+ if (mSearchResult == RESULT_NOMATCH && fileName.Equals(mSearchString))
+ mSearchResult = RESULT_IGNORED;
+ else
+ mSearchResult = RESULT_SUCCESS;
+ bool isDirectory = false;
+ nextFile->IsDirectory(&isDirectory);
+ if (isDirectory)
+ fileName.Append('/');
+ mValues.AppendElement(fileName);
+ }
+ }
+ mValues.Sort();
+ }
+}
+
+NS_IMETHODIMP nsFileResult::GetSearchString(nsAString & aSearchString)
+{
+ aSearchString.Assign(mSearchString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetSearchResult(uint16_t *aSearchResult)
+{
+ NS_ENSURE_ARG_POINTER(aSearchResult);
+ *aSearchResult = mSearchResult;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetDefaultIndex(int32_t *aDefaultIndex)
+{
+ NS_ENSURE_ARG_POINTER(aDefaultIndex);
+ *aDefaultIndex = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetErrorDescription(nsAString & aErrorDescription)
+{
+ aErrorDescription.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetMatchCount(uint32_t *aMatchCount)
+{
+ NS_ENSURE_ARG_POINTER(aMatchCount);
+ *aMatchCount = mValues.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetValueAt(int32_t index, nsAString & aValue)
+{
+ aValue = mValues[index];
+ if (aValue.Last() == '/')
+ aValue.Truncate(aValue.Length() - 1);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetLabelAt(int32_t index, nsAString & aValue)
+{
+ return GetValueAt(index, aValue);
+}
+
+NS_IMETHODIMP nsFileResult::GetCommentAt(int32_t index, nsAString & aComment)
+{
+ aComment.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetStyleAt(int32_t index, nsAString & aStyle)
+{
+ if (mValues[index].Last() == '/')
+ aStyle.AssignLiteral("directory");
+ else
+ aStyle.AssignLiteral("file");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetImageAt(int32_t index, nsAString & aImage)
+{
+ aImage.Truncate();
+ return NS_OK;
+}
+NS_IMETHODIMP nsFileResult::GetFinalCompleteValueAt(int32_t index,
+ nsAString & aValue)
+{
+ return GetValueAt(index, aValue);
+}
+
+NS_IMETHODIMP nsFileResult::RemoveValueAt(int32_t rowIndex, bool removeFromDb)
+{
+ return NS_OK;
+}
+
+class nsFileComplete final : public nsIAutoCompleteSearch
+{
+ ~nsFileComplete() {}
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAUTOCOMPLETESEARCH
+};
+
+NS_IMPL_ISUPPORTS(nsFileComplete, nsIAutoCompleteSearch)
+
+NS_IMETHODIMP
+nsFileComplete::StartSearch(const nsAString& aSearchString,
+ const nsAString& aSearchParam,
+ nsIAutoCompleteResult *aPreviousResult,
+ nsIAutoCompleteObserver *aListener)
+{
+ NS_ENSURE_ARG_POINTER(aListener);
+ RefPtr<nsFileResult> result = new nsFileResult(aSearchString, aSearchParam);
+ NS_ENSURE_TRUE(result, NS_ERROR_OUT_OF_MEMORY);
+ return aListener->OnSearchResult(this, result);
+}
+
+NS_IMETHODIMP
+nsFileComplete::StopSearch()
+{
+ return NS_OK;
+}
+
+#define NS_FILEVIEW_CID { 0xa5570462, 0x1dd1, 0x11b2, \
+ { 0x9d, 0x19, 0xdf, 0x30, 0xa2, 0x7f, 0xbd, 0xc4 } }
+
+class nsFileView : public nsIFileView,
+ public nsITreeView
+{
+public:
+ nsFileView();
+ nsresult Init();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFILEVIEW
+ NS_DECL_NSITREEVIEW
+
+protected:
+ virtual ~nsFileView();
+
+ void FilterFiles();
+ void ReverseArray(nsTArray<nsCOMPtr<nsIFile> >& aArray);
+ void SortArray(nsTArray<nsCOMPtr<nsIFile> >& aArray);
+ void SortInternal();
+
+ nsTArray<nsCOMPtr<nsIFile> > mFileList;
+ nsTArray<nsCOMPtr<nsIFile> > mDirList;
+ nsTArray<nsCOMPtr<nsIFile> > mFilteredFiles;
+
+ nsCOMPtr<nsIFile> mDirectoryPath;
+ nsCOMPtr<nsITreeBoxObject> mTree;
+ nsCOMPtr<nsITreeSelection> mSelection;
+ nsCOMPtr<nsIDateTimeFormat> mDateFormatter;
+
+ int16_t mSortType;
+ int32_t mTotalRows;
+
+ nsTArray<char16_t*> mCurrentFilters;
+
+ bool mShowHiddenFiles;
+ bool mDirectoryFilter;
+ bool mReverseSort;
+};
+
+// Factory constructor
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsFileComplete)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsFileView, Init)
+NS_DEFINE_NAMED_CID(NS_FILECOMPLETE_CID);
+NS_DEFINE_NAMED_CID(NS_FILEVIEW_CID);
+
+static const mozilla::Module::CIDEntry kFileViewCIDs[] = {
+ { &kNS_FILECOMPLETE_CID, false, nullptr, nsFileCompleteConstructor },
+ { &kNS_FILEVIEW_CID, false, nullptr, nsFileViewConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kFileViewContracts[] = {
+ { NS_FILECOMPLETE_CONTRACTID, &kNS_FILECOMPLETE_CID },
+ { NS_FILEVIEW_CONTRACTID, &kNS_FILEVIEW_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kFileViewModule = {
+ mozilla::Module::kVersion,
+ kFileViewCIDs,
+ kFileViewContracts
+};
+
+NSMODULE_DEFN(nsFileViewModule) = &kFileViewModule;
+
+nsFileView::nsFileView() :
+ mSortType(-1),
+ mTotalRows(0),
+ mShowHiddenFiles(false),
+ mDirectoryFilter(false),
+ mReverseSort(false)
+{
+}
+
+nsFileView::~nsFileView()
+{
+ uint32_t count = mCurrentFilters.Length();
+ for (uint32_t i = 0; i < count; ++i)
+ free(mCurrentFilters[i]);
+}
+
+nsresult
+nsFileView::Init()
+{
+ mDateFormatter = nsIDateTimeFormat::Create();
+ if (!mDateFormatter)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ return NS_OK;
+}
+
+// nsISupports implementation
+
+NS_IMPL_ISUPPORTS(nsFileView, nsITreeView, nsIFileView)
+
+// nsIFileView implementation
+
+NS_IMETHODIMP
+nsFileView::SetShowHiddenFiles(bool aShowHidden)
+{
+ if (aShowHidden != mShowHiddenFiles) {
+ mShowHiddenFiles = aShowHidden;
+
+ // This could be better optimized, but since the hidden
+ // file functionality is not currently used, this will be fine.
+ SetDirectory(mDirectoryPath);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetShowHiddenFiles(bool* aShowHidden)
+{
+ *aShowHidden = mShowHiddenFiles;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetShowOnlyDirectories(bool aOnlyDirs)
+{
+ if (aOnlyDirs == mDirectoryFilter)
+ return NS_OK;
+
+ mDirectoryFilter = aOnlyDirs;
+ uint32_t dirCount = mDirList.Length();
+ if (mDirectoryFilter) {
+ int32_t rowDiff = mTotalRows - dirCount;
+
+ mFilteredFiles.Clear();
+ mTotalRows = dirCount;
+ if (mTree)
+ mTree->RowCountChanged(mTotalRows, -rowDiff);
+ } else {
+ // Run the filter again to get the file list back
+ FilterFiles();
+
+ SortArray(mFilteredFiles);
+ if (mReverseSort)
+ ReverseArray(mFilteredFiles);
+
+ if (mTree)
+ mTree->RowCountChanged(dirCount, mTotalRows - dirCount);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetShowOnlyDirectories(bool* aOnlyDirs)
+{
+ *aOnlyDirs = mDirectoryFilter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetSortType(int16_t* aSortType)
+{
+ *aSortType = mSortType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetReverseSort(bool* aReverseSort)
+{
+ *aReverseSort = mReverseSort;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::Sort(int16_t aSortType, bool aReverseSort)
+{
+ if (aSortType == mSortType) {
+ if (aReverseSort == mReverseSort)
+ return NS_OK;
+
+ mReverseSort = aReverseSort;
+ ReverseArray(mDirList);
+ ReverseArray(mFilteredFiles);
+ } else {
+ mSortType = aSortType;
+ mReverseSort = aReverseSort;
+ SortInternal();
+ }
+
+ if (mTree)
+ mTree->Invalidate();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetDirectory(nsIFile* aDirectory)
+{
+ NS_ENSURE_ARG_POINTER(aDirectory);
+
+ nsCOMPtr<nsISimpleEnumerator> dirEntries;
+ aDirectory->GetDirectoryEntries(getter_AddRefs(dirEntries));
+
+ if (!dirEntries) {
+ // Couldn't read in the directory, this can happen if the user does not
+ // have permission to list it.
+ return NS_ERROR_FAILURE;
+ }
+
+ mDirectoryPath = aDirectory;
+ mFileList.Clear();
+ mDirList.Clear();
+
+ bool hasMore = false;
+
+ while (NS_SUCCEEDED(dirEntries->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> nextItem;
+ dirEntries->GetNext(getter_AddRefs(nextItem));
+ nsCOMPtr<nsIFile> theFile = do_QueryInterface(nextItem);
+
+ bool isDirectory = false;
+ if (theFile) {
+ theFile->IsDirectory(&isDirectory);
+
+ if (isDirectory) {
+ bool isHidden;
+ theFile->IsHidden(&isHidden);
+ if (mShowHiddenFiles || !isHidden) {
+ mDirList.AppendElement(theFile);
+ }
+ }
+ else {
+ mFileList.AppendElement(theFile);
+ }
+ }
+ }
+
+ if (mTree) {
+ mTree->BeginUpdateBatch();
+ mTree->RowCountChanged(0, -mTotalRows);
+ }
+
+ FilterFiles();
+ SortInternal();
+
+ if (mTree) {
+ mTree->EndUpdateBatch();
+ mTree->ScrollToRow(0);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetFilter(const nsAString& aFilterString)
+{
+ uint32_t filterCount = mCurrentFilters.Length();
+ for (uint32_t i = 0; i < filterCount; ++i)
+ free(mCurrentFilters[i]);
+ mCurrentFilters.Clear();
+
+ nsAString::const_iterator start, iter, end;
+ aFilterString.BeginReading(iter);
+ aFilterString.EndReading(end);
+
+ while (true) {
+ // skip over delimiters
+ while (iter != end && (*iter == ';' || *iter == ' '))
+ ++iter;
+
+ if (iter == end)
+ break;
+
+ start = iter; // start of a filter
+
+ // we know this is neither ';' nor ' ', skip to next char
+ ++iter;
+
+ // find next delimiter or end of string
+ while (iter != end && (*iter != ';' && *iter != ' '))
+ ++iter;
+
+ char16_t* filter = ToNewUnicode(Substring(start, iter));
+ if (!filter)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ if (!mCurrentFilters.AppendElement(filter)) {
+ free(filter);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (iter == end)
+ break;
+
+ ++iter; // we know this is either ';' or ' ', skip to next char
+ }
+
+ if (mTree) {
+ mTree->BeginUpdateBatch();
+ uint32_t count = mDirList.Length();
+ mTree->RowCountChanged(count, count - mTotalRows);
+ }
+
+ mFilteredFiles.Clear();
+
+ FilterFiles();
+
+ SortArray(mFilteredFiles);
+ if (mReverseSort)
+ ReverseArray(mFilteredFiles);
+
+ if (mTree)
+ mTree->EndUpdateBatch();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetSelectedFiles(nsIArray** aFiles)
+{
+ *aFiles = nullptr;
+ if (!mSelection)
+ return NS_OK;
+
+ int32_t numRanges;
+ mSelection->GetRangeCount(&numRanges);
+
+ uint32_t dirCount = mDirList.Length();
+ nsCOMPtr<nsIMutableArray> fileArray =
+ do_CreateInstance(NS_ARRAY_CONTRACTID);
+ NS_ENSURE_STATE(fileArray);
+
+ for (int32_t range = 0; range < numRanges; ++range) {
+ int32_t rangeBegin, rangeEnd;
+ mSelection->GetRangeAt(range, &rangeBegin, &rangeEnd);
+
+ for (int32_t itemIndex = rangeBegin; itemIndex <= rangeEnd; ++itemIndex) {
+ nsIFile* curFile = nullptr;
+
+ if (itemIndex < (int32_t) dirCount)
+ curFile = mDirList[itemIndex];
+ else {
+ if (itemIndex < mTotalRows)
+ curFile = mFilteredFiles[itemIndex - dirCount];
+ }
+
+ if (curFile)
+ fileArray->AppendElement(curFile, false);
+ }
+ }
+
+ fileArray.forget(aFiles);
+ return NS_OK;
+}
+
+
+// nsITreeView implementation
+
+NS_IMETHODIMP
+nsFileView::GetRowCount(int32_t* aRowCount)
+{
+ *aRowCount = mTotalRows;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetSelection(nsITreeSelection** aSelection)
+{
+ *aSelection = mSelection;
+ NS_IF_ADDREF(*aSelection);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetSelection(nsITreeSelection* aSelection)
+{
+ mSelection = aSelection;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetRowProperties(int32_t aIndex, nsAString& aProps)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetCellProperties(int32_t aRow, nsITreeColumn* aCol,
+ nsAString& aProps)
+{
+ uint32_t dirCount = mDirList.Length();
+
+ if (aRow < (int32_t) dirCount)
+ aProps.AppendLiteral("directory");
+ else if (aRow < mTotalRows)
+ aProps.AppendLiteral("file");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetColumnProperties(nsITreeColumn* aCol, nsAString& aProps)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsContainer(int32_t aIndex, bool* aIsContainer)
+{
+ *aIsContainer = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsContainerOpen(int32_t aIndex, bool* aIsOpen)
+{
+ *aIsOpen = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsContainerEmpty(int32_t aIndex, bool* aIsEmpty)
+{
+ *aIsEmpty = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsSeparator(int32_t aIndex, bool* aIsSeparator)
+{
+ *aIsSeparator = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsSorted(bool* aIsSorted)
+{
+ *aIsSorted = (mSortType >= 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::CanDrop(int32_t aIndex, int32_t aOrientation,
+ nsIDOMDataTransfer* dataTransfer, bool* aCanDrop)
+{
+ *aCanDrop = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::Drop(int32_t aRow, int32_t aOrientation, nsIDOMDataTransfer* dataTransfer)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetParentIndex(int32_t aRowIndex, int32_t* aParentIndex)
+{
+ *aParentIndex = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex,
+ bool* aHasSibling)
+{
+ *aHasSibling = (aAfterIndex < (mTotalRows - 1));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetLevel(int32_t aIndex, int32_t* aLevel)
+{
+ *aLevel = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetImageSrc(int32_t aRow, nsITreeColumn* aCol,
+ nsAString& aImageSrc)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetProgressMode(int32_t aRow, nsITreeColumn* aCol,
+ int32_t* aProgressMode)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetCellValue(int32_t aRow, nsITreeColumn* aCol,
+ nsAString& aCellValue)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetCellText(int32_t aRow, nsITreeColumn* aCol,
+ nsAString& aCellText)
+{
+ uint32_t dirCount = mDirList.Length();
+ bool isDirectory;
+ nsIFile* curFile = nullptr;
+
+ if (aRow < (int32_t) dirCount) {
+ isDirectory = true;
+ curFile = mDirList[aRow];
+ } else if (aRow < mTotalRows) {
+ isDirectory = false;
+ curFile = mFilteredFiles[aRow - dirCount];
+ } else {
+ // invalid row
+ aCellText.SetCapacity(0);
+ return NS_OK;
+ }
+
+ const char16_t* colID;
+ aCol->GetIdConst(&colID);
+ if (NS_LITERAL_STRING("FilenameColumn").Equals(colID)) {
+ curFile->GetLeafName(aCellText);
+ } else if (NS_LITERAL_STRING("LastModifiedColumn").Equals(colID)) {
+ PRTime lastModTime;
+ curFile->GetLastModifiedTime(&lastModTime);
+ // XXX FormatPRTime could take an nsAString&
+ nsAutoString temp;
+ mDateFormatter->FormatPRTime(nullptr, kDateFormatShort, kTimeFormatSeconds,
+ lastModTime * 1000, temp);
+ aCellText = temp;
+ } else {
+ // file size
+ if (isDirectory)
+ aCellText.SetCapacity(0);
+ else {
+ int64_t fileSize;
+ curFile->GetFileSize(&fileSize);
+ CopyUTF8toUTF16(nsPrintfCString("%lld", fileSize), aCellText);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetTree(nsITreeBoxObject* aTree)
+{
+ mTree = aTree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::ToggleOpenState(int32_t aIndex)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::CycleHeader(nsITreeColumn* aCol)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SelectionChanged()
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::CycleCell(int32_t aRow, nsITreeColumn* aCol)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsEditable(int32_t aRow, nsITreeColumn* aCol,
+ bool* aIsEditable)
+{
+ *aIsEditable = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsSelectable(int32_t aRow, nsITreeColumn* aCol,
+ bool* aIsSelectable)
+{
+ *aIsSelectable = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetCellValue(int32_t aRow, nsITreeColumn* aCol,
+ const nsAString& aValue)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetCellText(int32_t aRow, nsITreeColumn* aCol,
+ const nsAString& aValue)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::PerformAction(const char16_t* aAction)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::PerformActionOnRow(const char16_t* aAction, int32_t aRow)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::PerformActionOnCell(const char16_t* aAction, int32_t aRow,
+ nsITreeColumn* aCol)
+{
+ return NS_OK;
+}
+
+// Private methods
+
+void
+nsFileView::FilterFiles()
+{
+ uint32_t count = mDirList.Length();
+ mTotalRows = count;
+ count = mFileList.Length();
+ mFilteredFiles.Clear();
+ uint32_t filterCount = mCurrentFilters.Length();
+
+ for (uint32_t i = 0; i < count; ++i) {
+ nsIFile* file = mFileList[i];
+ bool isHidden = false;
+ if (!mShowHiddenFiles)
+ file->IsHidden(&isHidden);
+
+ nsAutoString ucsLeafName;
+ if(NS_FAILED(file->GetLeafName(ucsLeafName))) {
+ // need to check return value for GetLeafName()
+ continue;
+ }
+
+ if (!isHidden) {
+ for (uint32_t j = 0; j < filterCount; ++j) {
+ bool matched = false;
+ if (!nsCRT::strcmp(mCurrentFilters.ElementAt(j),
+ u"..apps"))
+ {
+ file->IsExecutable(&matched);
+ } else
+ matched = (NS_WildCardMatch(ucsLeafName.get(),
+ mCurrentFilters.ElementAt(j),
+ true) == MATCH);
+
+ if (matched) {
+ mFilteredFiles.AppendElement(file);
+ ++mTotalRows;
+ break;
+ }
+ }
+ }
+ }
+}
+
+void
+nsFileView::ReverseArray(nsTArray<nsCOMPtr<nsIFile> >& aArray)
+{
+ uint32_t count = aArray.Length();
+ for (uint32_t i = 0; i < count/2; ++i) {
+ // If we get references to the COMPtrs in the array, and then .swap() them
+ // we avoid AdRef() / Release() calls.
+ nsCOMPtr<nsIFile>& element = aArray[i];
+ nsCOMPtr<nsIFile>& element2 = aArray[count - i - 1];
+ element.swap(element2);
+ }
+}
+
+static int
+SortNameCallback(const void* aElement1, const void* aElement2, void* aContext)
+{
+ nsIFile* file1 = *static_cast<nsIFile* const *>(aElement1);
+ nsIFile* file2 = *static_cast<nsIFile* const *>(aElement2);
+
+ nsAutoString leafName1, leafName2;
+ file1->GetLeafName(leafName1);
+ file2->GetLeafName(leafName2);
+
+ return Compare(leafName1, leafName2);
+}
+
+static int
+SortSizeCallback(const void* aElement1, const void* aElement2, void* aContext)
+{
+ nsIFile* file1 = *static_cast<nsIFile* const *>(aElement1);
+ nsIFile* file2 = *static_cast<nsIFile* const *>(aElement2);
+
+ int64_t size1, size2;
+ file1->GetFileSize(&size1);
+ file2->GetFileSize(&size2);
+
+ if (size1 == size2)
+ return 0;
+
+ return size1 < size2 ? -1 : 1;
+}
+
+static int
+SortDateCallback(const void* aElement1, const void* aElement2, void* aContext)
+{
+ nsIFile* file1 = *static_cast<nsIFile* const *>(aElement1);
+ nsIFile* file2 = *static_cast<nsIFile* const *>(aElement2);
+
+ PRTime time1, time2;
+ file1->GetLastModifiedTime(&time1);
+ file2->GetLastModifiedTime(&time2);
+
+ if (time1 == time2)
+ return 0;
+
+ return time1 < time2 ? -1 : 1;
+}
+
+void
+nsFileView::SortArray(nsTArray<nsCOMPtr<nsIFile> >& aArray)
+{
+ // We assume the array to be in filesystem order, which
+ // for our purposes, is completely unordered.
+
+ int (*compareFunc)(const void*, const void*, void*);
+
+ switch (mSortType) {
+ case sortName:
+ compareFunc = SortNameCallback;
+ break;
+ case sortSize:
+ compareFunc = SortSizeCallback;
+ break;
+ case sortDate:
+ compareFunc = SortDateCallback;
+ break;
+ default:
+ return;
+ }
+
+ uint32_t count = aArray.Length();
+
+ nsIFile** array = new nsIFile*[count];
+ for (uint32_t i = 0; i < count; ++i) {
+ array[i] = aArray[i];
+ }
+
+ NS_QuickSort(array, count, sizeof(nsIFile*), compareFunc, nullptr);
+
+ for (uint32_t i = 0; i < count; ++i) {
+ // Use swap() to avoid refcounting.
+ aArray[i].swap(array[i]);
+ }
+
+ delete[] array;
+}
+
+void
+nsFileView::SortInternal()
+{
+ SortArray(mDirList);
+ SortArray(mFilteredFiles);
+
+ if (mReverseSort) {
+ ReverseArray(mDirList);
+ ReverseArray(mFilteredFiles);
+ }
+}
diff --git a/toolkit/components/filepicker/nsIFileView.idl b/toolkit/components/filepicker/nsIFileView.idl
new file mode 100644
index 0000000000..4862a594e0
--- /dev/null
+++ b/toolkit/components/filepicker/nsIFileView.idl
@@ -0,0 +1,34 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIArray;
+interface nsIFile;
+
+[scriptable, uuid(60b320d2-1dd2-11b2-bd73-dc3575f78ddd)]
+interface nsIFileView : nsISupports
+{
+ const short sortName = 0;
+ const short sortSize = 1;
+ const short sortDate = 2;
+
+ attribute boolean showHiddenFiles;
+ attribute boolean showOnlyDirectories;
+ readonly attribute short sortType;
+ readonly attribute boolean reverseSort;
+
+ void sort(in short sortType, in boolean reverseSort);
+ void setDirectory(in nsIFile directory);
+ void setFilter(in AString filterString);
+
+ readonly attribute nsIArray selectedFiles;
+};
+
+%{C++
+
+#define NS_FILEVIEW_CONTRACTID "@mozilla.org/filepicker/fileview;1"
+
+%}
diff --git a/toolkit/components/filepicker/test/unit/.eslintrc.js b/toolkit/components/filepicker/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/filepicker/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/filepicker/test/unit/test_filecomplete.js b/toolkit/components/filepicker/test/unit/test_filecomplete.js
new file mode 100644
index 0000000000..d1e18d5337
--- /dev/null
+++ b/toolkit/components/filepicker/test/unit/test_filecomplete.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Start by getting an empty directory.
+var dir = do_get_profile();
+dir.append("temp");
+dir.create(dir.DIRECTORY_TYPE, -1);
+var path = dir.path + "/";
+
+// Now create some sample entries.
+var file = dir.clone();
+file.append("test_file");
+file.create(file.NORMAL_FILE_TYPE, -1);
+file = dir.clone();
+file.append("other_file");
+file.create(file.NORMAL_FILE_TYPE, -1);
+dir.append("test_dir");
+dir.create(dir.DIRECTORY_TYPE, -1);
+
+var gListener = {
+ onSearchResult: function(aSearch, aResult) {
+ // Check that we got same search string back.
+ do_check_eq(aResult.searchString, "test");
+ // Check that the search succeeded.
+ do_check_eq(aResult.searchResult, aResult.RESULT_SUCCESS);
+ // Check that we got two results.
+ do_check_eq(aResult.matchCount, 2);
+ // Check that the first result is the directory we created.
+ do_check_eq(aResult.getValueAt(0), "test_dir");
+ // Check that the first result has directory style.
+ do_check_eq(aResult.getStyleAt(0), "directory");
+ // Check that the second result is the file we created.
+ do_check_eq(aResult.getValueAt(1), "test_file");
+ // Check that the second result has file style.
+ do_check_eq(aResult.getStyleAt(1), "file");
+ }
+};
+
+function run_test()
+{
+ Components.classes["@mozilla.org/autocomplete/search;1?name=file"]
+ .getService(Components.interfaces.nsIAutoCompleteSearch)
+ .startSearch("test", path, null, gListener);
+}
diff --git a/toolkit/components/filepicker/test/unit/xpcshell.ini b/toolkit/components/filepicker/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..1a0a002dc9
--- /dev/null
+++ b/toolkit/components/filepicker/test/unit/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head =
+tail =
+skip-if = toolkit == 'android'
+
+[test_filecomplete.js]
+skip-if = os != 'linux'