blob: 46b326da2fc8666e897739ac1fc3ce49343d0367 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package mx.controls
{
import flash.events.FileListEvent;
import flash.events.KeyboardEvent;
import flash.filesystem.File;
import flash.ui.Keyboard;
import mx.collections.ArrayCollection;
import mx.collections.Sort;
import mx.controls.fileSystemClasses.FileSystemControlHelper;
import mx.controls.fileSystemClasses.FileSystemTreeDataDescriptor;
import mx.controls.Tree;
import mx.core.mx_internal;
import mx.core.ScrollPolicy;
import mx.events.FileEvent;
import mx.events.ListEvent;
import mx.events.TreeEvent;
import mx.styles.StyleManager;
import mx.styles.CSSStyleDeclaration;
use namespace mx_internal;
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched whenever the <code>directory</code> property changes
* for any reason.
*
* @eventType mx.events.FileEvent.DIRECTORY_CHANGE
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="directoryChange", type="mx.events.FileEvent")]
/**
* Dispatched when the user closes an open directory node
* using the mouse of keyboard.
*
* @eventType mx.events.FileEvent.DIRECTORY_CLOSING
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="directoryClosing", type="mx.events.FileEvent")]
/**
* Dispatched when the user opens a directory node
* using the mouse or keyboard.
*
* <p>This is a cancelable event.
* If you call <code>event.preventDefault()</code>,
* this control continues to display the current directory
* rather than changing to display the subdirectory which was
* double-clicked.</p>
*
* @eventType mx.events.FileEvent.DIRECTORY_OPENING
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="directoryOpening", type="mx.events.FileEvent")]
/**
* Dispatched when the user chooses a file by double-clicking it
* or by selecting it and pressing Enter.
*
* @eventType mx.events.FileEvent.FILE_CHOOSE
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="fileChoose", type="mx.events.FileEvent")]
//--------------------------------------
// Other metadata
//--------------------------------------
[IconFile("FileSystemTree.png")]
[ResourceBundle("aircontrols")]
/**
* The FileSystemTree control displays the contents of a
* file system directory as a tree.
*
* <p>You specify the directory whose content is displayed by setting
* the <code>directory</code> property to an instance
* of the flash.filesystem.File class.
* (File instances can represent directories as well as files.)
* Whenever this property changes for any reason, the control
* dispatches a <code>directoryChange</code> event.</p>
*
* <p>You can set the <code>enumerationMode</code> property to specify
* whether to show this directory's files, its subdirectories, or both.
* There are three ways to show both files and subdirectories within
* each tree node: directories first, files first, or intermixed.</p>
*
* <p>You can set the <code>extensions</code> property to filter the list
* so that only files with the specified extensions are displayed.
* (Extensions on directories are ignored.)
* You can also specify an additional filtering function of your own
* by setting the <code>filterFunction</code> property.</p>
*
* <p>You can use the <code>showExtensions</code> property to show
* or hide file extensions, and the <code>showIcons</code> property
* to show or hide icons.</p>
*
* <p>You can do custom-sorting within each tree node by setting
* the <code>nameCompareFunction</code> property to a function
* that compares two file or directory names.</p>
*
* <p>If the user double-clicks a closed directory node,
* or clicks its disclosure icon,
* this control dispatches a <code>directoryOpening</code> event.
* If the user double-clicks an open directory node,
* or clicks its disclosure icon,
* this control dispatches a <code>directoryClosing</code> event.
* A handler can cancel either event by calling
* <code>event.preventDefault()</code> in which case the node doesn't open.</p>
*
* <p>If the user double-clicks a file node,
* this control dispatches a <code>select</code> event.</p>
*
* @mxml
*
* <p>The <code>&lt;mx:FileSystemTree&gt;</code> tag inherits all of the tag
* attributes of its superclass and adds the following tag attributes:</p>
*
* <pre>
* &lt;mx:FileSystemTree
* <strong>Properties</strong>
* directory="<i>null</i>"
* enumerationMode="directoriesFirst"
* extensions="<i>null</i>"
* filterFunction="<i>null</i>"
* nameCompareFunction="<i>null</i>"
* openPaths="<i>null</i>"
* selectedPath="<i>null</i>"
* selectedPaths="<i>null</i>"
* showExtensions="true"
* showHidden="false"
* showIcons="true"
*
* <strong>Events</strong>
* directoryChange="<i>No default</i>"
* directoryClosing="<i>No default</i>"
* directoryOpening="<i>No default</i>"
* fileChoose="<i>No default</i>"
* /&gt;
* </pre>
*
* @see flash.filesystem.File
*
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public class FileSystemTree extends Tree
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Class constants
//
//--------------------------------------------------------------------------
/**
* @copy mx.controls.FileSystemList#COMPUTER
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static const COMPUTER:File = FileSystemControlHelper.COMPUTER;
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function FileSystemTree()
{
super();
helper = new FileSystemControlHelper(this, true);
dataDescriptor = new FileSystemTreeDataDescriptor();
doubleClickEnabled = true;
horizontalScrollPolicy = ScrollPolicy.AUTO;
iconFunction = helper.fileIconFunction;
labelFunction = helper.fileLabelFunction;
addEventListener(TreeEvent.ITEM_OPENING, itemOpeningHandler);
addEventListener(ListEvent.ITEM_DOUBLE_CLICK, itemDoubleClickHandler);
// Set the initial dataProvider by enumerating the root directories.
directory = COMPUTER;
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
*/
mx_internal var helper:FileSystemControlHelper;
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// directory
//----------------------------------
[Bindable("directoryChanged")]
/**
* The directory whose contents this control displays.
*
* <p>If you set this property to a File object representing
* an existing directory, the <code>dataProvider</code>
* immediately becomes <code>null</code>.
* Later, when this control is revalidated by the LayoutManager,
* it performs a synchronous enumeration of that directory's
* contents and populates the <code>dataProvider</code> property
* with an ArrayCollection of the resulting File objects
* for the directory's files and subdirectories.</p>
*
* <p>Setting this to a File which does not represent
* an existing directory is an error.
* Setting this to <code>COMPUTER</code> synchronously displays
* the root directories, such as C: and D: on Windows.</p>
*
* @default COMPUTER
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get directory():File
{
return helper.directory;
}
/**
* @private
*/
public function set directory(value:File):void
{
helper.directory = value;
}
//----------------------------------
// enumerationMode
//----------------------------------
/**
* @copy mx.controls.FileSystemList#enumerationMode
*
* @default FileSystemEnumerationMode.DIRECTORIES_FIRST
*
* @see mx.controls.FileSystemEnumerationMode
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get enumerationMode():String
{
return helper.enumerationMode;
}
/**
* @private
*/
public function set enumerationMode(value:String):void
{
helper.enumerationMode = value;
}
//----------------------------------
// extensions
//----------------------------------
/**
* @copy mx.controls.FileSystemList#extensions
*
* @default null
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get extensions():Array /* of String */
{
return helper.extensions;
}
/**
* @private
*/
public function set extensions(value:Array /* of String */):void
{
helper.extensions = value;
}
//----------------------------------
// filterFunction
//----------------------------------
/**
* @copy mx.controls.FileSystemList#filterFunction
*
* @default null
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get filterFunction():Function
{
return helper.filterFunction;
}
/**
* @private
*/
public function set filterFunction(value:Function):void
{
helper.filterFunction = value;
}
//----------------------------------
// nameCompareFunction
//----------------------------------
/**
* @copy mx.controls.FileSystemList#nameCompareFunction
*
* @default null
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get nameCompareFunction():Function
{
return helper.nameCompareFunction;
}
/**
* @private
*/
public function set nameCompareFunction(value:Function):void
{
helper.nameCompareFunction = value;
}
//----------------------------------
// openPaths
//----------------------------------
/**
* An Array of <code>nativePath</code> Strings for the File items
* representing the open subdirectories.
* This Array is empty if no subdirectories are open.
*
* @default []
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get openPaths():Array /* of String */
{
return helper.openPaths;
}
/**
* @private
*/
public function set openPaths(value:Array /* of String */):void
{
helper.openPaths = value;
}
//----------------------------------
// selectedPath
//----------------------------------
[Bindable("change")]
[Bindable("directoryChanged")]
/**
* @copy mx.controls.FileSystemList#selectedPath
*
* @default null
*
* @see mx.controls.listClasses.ListBase#selectedIndex
* @see mx.controls.listClasses.ListBase#selectedItem
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get selectedPath():String
{
return helper.selectedPath;
}
/**
* @private
*/
public function set selectedPath(value:String):void
{
helper.selectedPath = value;
}
//----------------------------------
// selectedPaths
//----------------------------------
[Bindable("change")]
[Bindable("directoryChanged")]
/**
* @copy mx.controls.FileSystemList#selectedPaths
*
* @default []
*
* @see mx.controls.listClasses.ListBase#selectedIndex
* @see mx.controls.listClasses.ListBase#selectedItem
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get selectedPaths():Array /* of String */
{
return helper.selectedPaths;
}
/**
* @private
*/
public function set selectedPaths(value:Array /* of String */):void
{
helper.selectedPaths = value;
}
//----------------------------------
// showExtensions
//----------------------------------
/**
* @copy mx.controls.FileSystemList#showExtensions
*
* @default true
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get showExtensions():Boolean
{
return helper.showExtensions;
}
/**
* @private
*/
public function set showExtensions(value:Boolean):void
{
helper.showExtensions = value;
}
//----------------------------------
// showHidden
//----------------------------------
/**
* @copy mx.controls.FileSystemList#showHidden
*
* @default false
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get showHidden():Boolean
{
return helper.showHidden;
}
/**
* @private
*/
public function set showHidden(value:Boolean):void
{
helper.showHidden = value;
}
//----------------------------------
// showIcons
//----------------------------------
/**
* @copy mx.controls.FileSystemList#showIcons
*
* @default true
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get showIcons():Boolean
{
return helper.showIcons;
}
/**
* @private
*/
public function set showIcons(value:Boolean):void
{
helper.showIcons = value;
}
//--------------------------------------------------------------------------
//
// Overridden methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
override protected function commitProperties():void
{
helper.commitProperties();
super.commitProperties();
}
/**
* @private
*/
override protected function measure():void
{
super.measure();
var text:String = resourceManager.getString(
"aircontrols", "fileSystemTree_measuredText");
measuredWidth = measureText(text).width;
}
/**
* @private
* The FileSystemControlHelper calls getStyle("directoryIcon")
* and getStyle("fileIcon") because these are the style names
* that FileSystemList and FileSystemDataGrid declare for their icons.
* But FileSystemTree extends Tree and Tree already declares
* the styles "folderClosedIcon" and "defaultLeafIcon"
* for these icons, so it doesn't declare "directoryIcon"
* and fileIcon.
* Therefore, we map the names here.
*/
override public function getStyle(styleProp:String):*
{
if (styleProp == "directoryIcon")
styleProp = "folderClosedIcon";
else if (styleProp == "fileIcon")
styleProp = "defaultLeafIcon";
return super.getStyle(styleProp);
}
/**
* @private
*/
override public function styleChanged(styleProp:String):void
{
super.styleChanged(styleProp);
helper.styleChanged(styleProp);
}
/**
* @private
*/
override protected function itemToUID(data:Object):String
{
return helper.itemToUID(data);
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @copy mx.controls.FileSystemList#findIndex()
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function findIndex(nativePath:String):int
{
return helper.findIndex(nativePath);
}
/**
* @copy mx.controls.FileSystemList#findItem()
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function findItem(nativePath:String):File
{
return helper.findItem(nativePath);
}
/**
* Re-enumerates the current directory being displayed by this control.
*
* <p>When this method returns, the <code>directory</code> property
* contains the File instance for the same directory as before.
* The <code>dataProvider</code> property is temporarily
* <code>null</code> until the directory is re-enumerated.
* After the enumeration, the <code>dataProvider</code> property
* contains an ArrayCollection of File instances
* for the directory's contents.</p>
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function refresh():void
{
helper.refresh();
}
/**
* Clears the list.
*
* <p>This method sets the <code>dataProvider</code> to <code>null</code>
* but leaves the <code>directory</code> property unchanged.
* You can call <code>refresh</code> to populate the list again.</p>
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function clear():void
{
helper.clear();
}
/**
* Opens a subdirectory specified by a native file system path.
*
* <p>This method automatically opens all intervening directories
* required to reach the specified directory.</p>
*
* <p>If the <code>nativePath</code> doesn't specify
* an existing file system directory, or if that
* directory isn't within the directory that this control
* is displaying, then this method does nothing.</p>
*
* @param file A String specifying the <code>nativePath</code>
* of a File item.
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function openSubdirectory(nativePath:String):void
{
var item:File = new File(nativePath);
if (!item.exists || !item.isDirectory)
return;
// Don't animate the opens, since we're doing
// multiple ones synchronously.
var savedOpenDuration:Number = getStyle("openDuration");
setStyle("openDuration", 0);
var parentPaths:Array = getParentPaths(item);
var n:int = parentPaths.length;
for (var i:int = 0; i < n; i++)
{
item = findItem(parentPaths[i]);
if (item)
openItem(item, false);
}
setStyle("openDuration", savedOpenDuration);
invalidateList();
}
/**
* @private
*/
mx_internal function openItem(item:File, async:Boolean = true):void
{
if (isItemOpen(item))
return;
var children:ArrayCollection =
ArrayCollection(dataDescriptor.getChildren(item));
if (!children || modificationDateHasChanged(item))
{
if (async)
{
item.addEventListener(FileListEvent.DIRECTORY_LISTING,
directoryListingHandler);
item.getDirectoryListingAsync();
}
else
{
insertChildItems(item, item.getDirectoryListing());
}
}
else
{
expandItem(item, true, true);
helper.itemsChanged();
}
invalidateList();
}
/**
* Closes a subdirectory specified by a native file system path.
*
* <p>If the <code>nativePath</code> doesn't specify
* a directory being displayed within this control,
* then this method does nothing.</p>
*
* @param file A String specifying the <code>nativePath</code>
* of a File item.
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function closeSubdirectory(nativePath:String):void
{
var item:File = findItem(nativePath);
if (item && item.isDirectory)
closeItem(item);
}
/**
* @private
*/
mx_internal function closeItem(item:File):void
{
expandItem(item, false, true);
fixSelectionAfterClose(item);
helper.itemsChanged();
}
/**
* @private
*
* If any selected items are inside the closing item,
* deselect them and select the closing item in their place.
*/
mx_internal function fixSelectionAfterClose(item:File):void
{
var closingPath:String = item.nativePath;
var newSelectedItems:Array = []
var n:int = selectedItems.length;
for (var i:int = 0; i < n; i++)
{
var selectedPath:String = selectedItems[i].nativePath;
if (selectedPath.indexOf(closingPath) == 0)
newSelectedItems.push(item);
else
newSelectedItems.push(selectedItems[i]);
}
selectedItems = newSelectedItems;
}
/**
* @private
*/
private function getParentPaths(file:File):Array /* of String */
{
var a:Array = [];
for (var f:File = file; f != null; f = f.parent)
{
if (f.nativePath == directory.nativePath)
break;
a.unshift(f.nativePath);
}
return a;
}
/**
* @private
*/
private function modificationDateHasChanged(item:File):Boolean
{
var item2:File = new File(item.nativePath);
return item.modificationDate != item2.modificationDate;
}
/**
* @private
*/
mx_internal function insertChildItems(subdirectory:File, childItems:Array):void
{
var childCollection:ArrayCollection = new ArrayCollection(childItems);
childCollection.filterFunction =
helper.directoryEnumeration.fileFilterFunction;
childCollection.sort = new Sort();
childCollection.sort.compareFunction =
helper.directoryEnumeration.fileCompareFunction;
childCollection.refresh();
FileSystemTreeDataDescriptor(dataDescriptor).setChildren(
subdirectory, childCollection);
expandItem(subdirectory, true, true);
helper.itemsChanged();
}
/**
* @private
*
* Opens the selected node if it is a directory.
* This method does nothing if no node is selected,
* or if a file node is selected.
*/
private function openSelectedSubdirectory():void
{
var item:File = File(selectedItem);
if (item && item.exists && item.isDirectory)
openItem(item, true);
}
/**
* @private
*
* Closes the selected node if it is a directory.
* This method does nothing if no node is selected,
* or if a file node is selected.
*/
private function closeSelectedSubdirectory():void
{
var item:File = File(selectedItem);
if (item && item.exists && item.isDirectory)
closeItem(item);
}
/**
* @private
* Dispatches a cancelable "directoryOpening" event
* and returns true if it wasn't canceled.
*/
private function dispatchDirectoryOpeningEvent(directory:File):Boolean
{
var event:FileEvent =
new FileEvent(FileEvent.DIRECTORY_OPENING, false, true);
event.file = directory;
dispatchEvent(event);
return !event.isDefaultPrevented();
}
/**
* @private
* Dispatches a cancelable "directoryClosing" event
* and returns true if it wasn't canceled.
*/
private function dispatchDirectoryClosingEvent(directory:File):Boolean
{
var event:FileEvent =
new FileEvent(FileEvent.DIRECTORY_CLOSING, false, true);
event.file = directory;
dispatchEvent(event);
return !event.isDefaultPrevented();
}
//--------------------------------------------------------------------------
//
// Overridden event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
override protected function keyDownHandler(event:KeyboardEvent):void
{
if (event.keyCode == Keyboard.ENTER)
{
var selectedFile:File = File(selectedItem);
if (selectedFile && !selectedFile.isDirectory)
helper.dispatchFileChooseEvent(selectedFile);
return;
}
super.keyDownHandler(event);
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
private function itemOpeningHandler(event:TreeEvent):void
{
event.preventDefault();
var item:File = File(event.item);
if (event.opening)
{
if (dispatchDirectoryOpeningEvent(item))
openItem(item, true);
}
else
{
if (dispatchDirectoryClosingEvent(item))
closeItem(item);
}
}
/**
* @private
* Completes an async openItem() call.
*/
private function directoryListingHandler(event:FileListEvent):void
{
insertChildItems(File(event.target), event.files);
}
/**
* @private
*/
private function itemDoubleClickHandler(event:ListEvent):void
{
var item:File = File(selectedItem);
if (item.isDirectory)
{
if (!isItemOpen(item))
{
// Dispatch a cancelable "directoryOpening" event.
// If the event wasn't canceled,
// then open that node and display its children.
if (dispatchDirectoryOpeningEvent(item))
openSelectedSubdirectory();
}
else
{
// Dispatch a cancelable "directoryClosing" event.
// If the event wasn't canceled,
// then close that node.
if (dispatchDirectoryClosingEvent(item))
closeSelectedSubdirectory();
}
}
else
{
helper.dispatchFileChooseEvent(item);
}
}
}
}