diff options
Diffstat (limited to 'components/filepicker')
-rw-r--r-- | components/filepicker/content/filepicker.js | 833 | ||||
-rw-r--r-- | components/filepicker/content/filepicker.xul | 80 | ||||
-rw-r--r-- | components/filepicker/jar.mn | 8 | ||||
-rw-r--r-- | components/filepicker/moz.build | 18 | ||||
-rw-r--r-- | components/filepicker/nsFilePicker.js | 319 | ||||
-rw-r--r-- | components/filepicker/nsFilePicker.manifest | 4 | ||||
-rw-r--r-- | components/filepicker/nsFileView.cpp | 989 | ||||
-rw-r--r-- | components/filepicker/nsIFileView.idl | 34 |
8 files changed, 2285 insertions, 0 deletions
diff --git a/components/filepicker/content/filepicker.js b/components/filepicker/content/filepicker.js new file mode 100644 index 000000000..6f91066ba --- /dev/null +++ b/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/components/filepicker/content/filepicker.xul b/components/filepicker/content/filepicker.xul new file mode 100644 index 000000000..4bf311230 --- /dev/null +++ b/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/components/filepicker/jar.mn b/components/filepicker/jar.mn new file mode 100644 index 000000000..ba585f3a4 --- /dev/null +++ b/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/components/filepicker/moz.build b/components/filepicker/moz.build new file mode 100644 index 000000000..50c344dc5 --- /dev/null +++ b/components/filepicker/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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_SOURCES += ['nsIFileView.idl'] + +XPIDL_MODULE = 'filepicker' + +SOURCES += ['nsFileView.cpp'] + +EXTRA_COMPONENTS += ['nsFilePicker.js'] + +EXTRA_PP_COMPONENTS += ['nsFilePicker.manifest'] + +FINAL_LIBRARY = 'xul' + +JAR_MANIFESTS += ['jar.mn'] diff --git a/components/filepicker/nsFilePicker.js b/components/filepicker/nsFilePicker.js new file mode 100644 index 000000000..8c92ff821 --- /dev/null +++ b/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/components/filepicker/nsFilePicker.manifest b/components/filepicker/nsFilePicker.manifest new file mode 100644 index 000000000..06c2e8956 --- /dev/null +++ b/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/components/filepicker/nsFileView.cpp b/components/filepicker/nsFileView.cpp new file mode 100644 index 000000000..9a8278496 --- /dev/null +++ b/components/filepicker/nsFileView.cpp @@ -0,0 +1,989 @@ +/* -*- 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::GetTypeAheadResult(bool *aTypeAheadResult) +{ + NS_ENSURE_ARG_POINTER(aTypeAheadResult); + *aTypeAheadResult = false; + 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/components/filepicker/nsIFileView.idl b/components/filepicker/nsIFileView.idl new file mode 100644 index 000000000..4862a594e --- /dev/null +++ b/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" + +%} |