////////////////////////////////////////////////////////////////////////////////
//
// 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.fileSystemClasses
{
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.filesystem.File;
import flash.system.Capabilities;
import flash.ui.Keyboard;
import mx.collections.ArrayCollection;
import mx.controls.FileSystemEnumerationMode;
import mx.controls.dataGridClasses.DataGridColumn;
import mx.core.mx_internal;
import mx.events.FileEvent;
import mx.events.FlexEvent;
import mx.events.ListEvent;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
import mx.utils.DirectoryEnumeration;
use namespace mx_internal;
[ExcludeClass]
/**
* @private
*/
public class FileSystemControlHelper
{
include "../../core/Version.as";
//--------------------------------------------------------------------------
//
// Class initialization
//
//--------------------------------------------------------------------------
/**
* @private
*/
public static var COMPUTER:File;
/**
* @private
*/
private static function initClass():void
{
if (Capabilities.os.substring(0, 3) == "Win")
COMPUTER = new File("root$:\\Computer");
else // Mac or Unix
COMPUTER = new File("/Computer");
}
initClass();
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
private static function fileSystemIsCaseInsensitive():Boolean
{
var os:String = Capabilities.os.substring(0, 3);
return os == "Win" || os == "Mac";
}
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function FileSystemControlHelper(owner:Object, hierarchical:Boolean)
{
super();
this.owner = owner;
this.hierarchical = hierarchical;
owner.addEventListener(FlexEvent.UPDATE_COMPLETE,
updateCompleteHandler);
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* A reference to the FileSystemList, FileSystemDataGrid,
* FileSystemTree, or FileSystemComboBox using this object.
*/
mx_internal var owner:Object;
/**
* @private
* A flag indicating whether the dataProvider of the owner
* is hierarchical or flat.
* In other words, this flag is true if the owner
* is a FileSystemTree and false otherwise.
*/
mx_internal var hierarchical:Boolean;
/**
* @private
*/
mx_internal var resourceManager:IResourceManager =
ResourceManager.getInstance();
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// backHistory
//----------------------------------
/**
* @private
*/
public function get backHistory():Array
{
return historyIndex > 0 ?
history.slice(0, historyIndex).reverse() :
[];
}
//----------------------------------
// canNavigateBack
//----------------------------------
/**
* @private
*/
public function get canNavigateBack():Boolean
{
return historyIndex > 0;
}
//----------------------------------
// canNavigateDown
//----------------------------------
/**
* @private
*/
public function get canNavigateDown():Boolean
{
var selectedFile:File = File(owner.selectedItem);
return selectedFile && selectedFile.isDirectory;
}
//----------------------------------
// canNavigateForward
//----------------------------------
/**
* @private
*/
public function get canNavigateForward():Boolean
{
return historyIndex < history.length - 1;
}
//----------------------------------
// canNavigateUp
//----------------------------------
/**
* @private
*/
public function get canNavigateUp():Boolean
{
return !isComputer(directory);
}
//----------------------------------
// directory
//----------------------------------
/**
* @private
* Storage for the directory property.
*/
private var _directory:File;
/**
* @private
*/
private var directoryChanged:Boolean = false;
/**
* @private
*/
public function get directory():File
{
return _directory;
}
/**
* @private
*/
public function set directory(value:File):void
{
if (!value ||
(!isComputer(value) &&
(!value.exists || !value.isDirectory)))
{
throw(new Error("No such directory: " + value.nativePath));
}
resetHistory(value);
setDirectory(value);
}
//----------------------------------
// directoryEnumeration
//----------------------------------
/**
* @private
*/
mx_internal var directoryEnumeration:DirectoryEnumeration =
new DirectoryEnumeration();
//----------------------------------
// enumerationMode
//----------------------------------
/**
* @private
* Storage for the enumerationMode property.
*/
private var _enumerationMode:String =
FileSystemEnumerationMode.DIRECTORIES_FIRST;
/**
* @private
*/
private var enumerationModeChanged:Boolean = false;
/**
* @private
*/
public function get enumerationMode():String
{
return _enumerationMode;
}
/**
* @private
*/
public function set enumerationMode(value:String):void
{
_enumerationMode = value;
enumerationModeChanged = true;
owner.invalidateProperties();
}
//----------------------------------
// extensions
//----------------------------------
/**
* @private
* Storage for the extensions property.
*/
private var _extensions:Array /* of String */;
/**
* @private
*/
private var extensionsChanged:Boolean = false;
/**
* @private
*/
public function get extensions():Array /* of String */
{
return _extensions;
}
/**
* @private
*/
public function set extensions(value:Array /* of String */):void
{
_extensions = value;
extensionsChanged = true;
owner.invalidateProperties();
}
//----------------------------------
// filterFunction
//----------------------------------
/**
* @private
* Storage for the filterFunction property.
*/
private var _filterFunction:Function;
/**
* @private
*/
private var filterFunctionChanged:Boolean = false;
/**
* @private
*/
public function get filterFunction():Function
{
return _filterFunction;
}
/**
* @private
*/
public function set filterFunction(value:Function):void
{
_filterFunction = value;
filterFunctionChanged = true;
owner.invalidateProperties();
}
//----------------------------------
// forwardHistory
//----------------------------------
[Bindable("historyChanged")]
/**
* @private
*/
public function get forwardHistory():Array
{
return historyIndex < history.length - 1 ?
history.slice(historyIndex + 1) :
[];
}
//----------------------------------
// history
//----------------------------------
/**
* @private
*/
public var history:Array;
//----------------------------------
// historyIndex
//----------------------------------
/**
* @private
*/
public var historyIndex:int;
//----------------------------------
// nativePathToIndexMap
//----------------------------------
/**
* @private
* Storage for the nativePathToIndexMap property.
*/
private var _nativePathToIndexMap:Object;
/**
* @private
* Maps nativePath (String) -> index (int).
* This map is used to implement findIndex() as a simple lookup,
* so that multiple finds are fast.
* It is freed whenever an operation changes which items
* are displayed in the control, or their order,
* and rebuilt tne next time it or items
is accessed.
*/
mx_internal function get nativePathToIndexMap():Object
{
if (!_nativePathToIndexMap)
rebuildEnumerationInfo();
return _nativePathToIndexMap;
}
//----------------------------------
// itemArray
//----------------------------------
/**
* @private
* Storage for the itemArray property.
*/
private var _itemArray:Array /* of File */;
/**
* @private
* An array of all the File items displayed in the control,
* in the order in which they appear.
* This array is used together with nativePathToIndexMap
* to implement findItem() as a simple lookup,
* so that multiple finds are fast.
* It is freed whenever an operation changes which items
* are displayed in the control, or their order,
* and rebuilt tne next time it
* or nativePathToIndexMap
is accessed.
*/
mx_internal function get itemArray():Array /* of File */
{
if (!_itemArray)
rebuildEnumerationInfo();
return _itemArray;
}
//----------------------------------
// nameCompareFunction
//----------------------------------
/**
* @private
* Storage for the nameCompareFunction property.
*/
private var _nameCompareFunction:Function;
/**
* @private
*/
private var nameCompareFunctionChanged:Boolean = false;
/**
* @private
*/
public function get nameCompareFunction():Function
{
return _nameCompareFunction;
}
/**
* @private
*/
public function set nameCompareFunction(value:Function):void
{
_nameCompareFunction = value;
nameCompareFunctionChanged = true;
owner.invalidateProperties();
}
//----------------------------------
// openPaths
//----------------------------------
/**
* @private
*/
private var pendingOpenPaths:Array /* of String */;
/**
* An Array of nativePath
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 pendingOpenPaths ?
pendingOpenPaths :
getOpenPaths();
}
/**
* @private
*/
public function set openPaths(value:Array /* of String */):void
{
pendingOpenPaths = value;
owner.invalidateProperties();
}
//----------------------------------
// selectedPath
//----------------------------------
/**
* @private
*/
public function get selectedPath():String
{
return selectedPaths[0];
}
/**
* @private
*/
public function set selectedPath(value:String):void
{
selectedPaths = [ value ];
}
//----------------------------------
// selectedPaths
//----------------------------------
/**
* @private
*/
private var pendingSelectedPaths:Array /* of String */;
/**
* @private
*/
public function get selectedPaths():Array /* of String */
{
return pendingSelectedPaths ?
pendingSelectedPaths :
getSelectedPaths();
}
/**
* @private
*/
public function set selectedPaths(value:Array /* of String */):void
{
pendingSelectedPaths = value;
owner.invalidateProperties();
}
//----------------------------------
// showExtensions
//----------------------------------
/**
* @private
* Storage for the showExtensions property.
*/
private var _showExtensions:Boolean = true;
/**
* @private
*/
public function get showExtensions():Boolean
{
return _showExtensions;
}
/**
* @private
*/
public function set showExtensions(value:Boolean):void
{
_showExtensions = value;
owner.invalidateList();
}
//----------------------------------
// showHidden
//----------------------------------
/**
* @private
* Storage for the showHidden property.
*/
private var _showHidden:Boolean = false;
/**
* @private
*/
private var showHiddenChanged:Boolean = false;
/**
* @private
*/
public function get showHidden():Boolean
{
return _showHidden;
}
/**
* @private
*/
public function set showHidden(value:Boolean):void
{
_showHidden = value;
showHiddenChanged = true;
owner.invalidateProperties();
}
//----------------------------------
// showIcons
//----------------------------------
/**
* @private
* Storage for the showIcons property.
*/
private var _showIcons:Boolean = true;
/**
* @private
*/
public function get showIcons():Boolean
{
return _showIcons;
}
/**
* @private
*/
public function set showIcons(value:Boolean):void
{
_showIcons = value;
owner.invalidateList();
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
public function commitProperties():void
{
if (enumerationModeChanged ||
extensionsChanged ||
filterFunctionChanged ||
nameCompareFunctionChanged ||
showHiddenChanged)
{
directoryEnumeration.enumerationMode = enumerationMode;
directoryEnumeration.extensions = extensions;
directoryEnumeration.filterFunction = filterFunction;
directoryEnumeration.nameCompareFunction = nameCompareFunction;
directoryEnumeration.showHidden = showHidden;
directoryEnumeration.refresh();
// For a List or DataGrid, refreshing its collection
// (which is what directoryEnumeration.refresh() does)
// is enough to make the control update properly
// with the newly filtered/sorted collection.
// But a Tree doesn't properly handle having its
// collection refreshed; for example, if the new
// filter reduces the number of items, the Tree
// can display blank renderers.
// So instead we simply reset the dataProvider.
owner.dataProvider = directoryEnumeration.collection;
itemsChanged();
extensionsChanged = false;
enumerationModeChanged = false;
filterFunctionChanged = false;
nameCompareFunctionChanged = false;
showHiddenChanged = false;
}
if (directoryChanged)
{
fill();
var event:FileEvent = new FileEvent(FileEvent.DIRECTORY_CHANGE);
event.file = directory;
owner.dispatchEvent(event);
directoryChanged = false;
}
}
/**
* Fills the list by enumerating the current directory
* and setting the dataProvider.
*
* @langversion 3.0
* @playerversion AIR 1.1
* @productversion Flex 3
*/
mx_internal function fill():void
{
setDataProvider(isComputer(directory) ?
getRootDirectories() :
directory.getDirectoryListing());
}
/**
* @private
*/
public function styleChanged(styleProp:String):void
{
if (styleProp == "fileIcon" || styleProp == "directoryIcon")
owner.invalidateList();
}
/**
* @private
*/
mx_internal function setDirectory(value:File):void
{
_directory = value;
directoryChanged = true;
// Clear the now-stale contents of the list.
// The list will repopulate after the new directory
// is enumerated.
owner.dataProvider = null;
if (hierarchical)
owner.dataDescriptor.reset();
owner.invalidateProperties();
// Trigger databindings.
owner.dispatchEvent(new Event("directoryChanged"));
}
/**
* @private
*/
mx_internal function setDataProvider(value:Array):void
{
directoryEnumeration.enumerationMode = enumerationMode;
directoryEnumeration.extensions = extensions;
directoryEnumeration.filterFunction = filterFunction;
directoryEnumeration.nameCompareFunction = nameCompareFunction;
directoryEnumeration.showHidden = showHidden;
directoryEnumeration.source = value;
owner.dataProvider = directoryEnumeration.collection;
itemsChanged();
}
/**
* @private
*/
public function itemToUID(data:Object):String
{
return data ? File(data).nativePath : "null";
}
/**
* @private
*/
public function isComputer(f:File):Boolean
{
if (Capabilities.os.substr(0, 3) =="Win")
return f.nativePath.substring(0, 6) == "root$:";
return f.nativePath == "/Computer";
}
/**
* @private
*/
private function getRootDirectories():Array
{
var a:Array = [];
for each (var f:File in File.getRootDirectories())
{
if (f.isDirectory)
a.push(f);
}
return a;
}
/**
* @private
*/
public function fileIconFunction(item:File):Class
{
if (!showIcons)
return null;
return owner.getStyle(item.isDirectory ? "directoryIcon" : "fileIcon");
}
/**
* @private
*/
public function fileLabelFunction(item:File,
column:DataGridColumn = null):String
{
if (isComputer(item))
{
return resourceManager.getString(
"aircontrols", "computer");
}
var label:String = item.name;
// The name of the / directory on Mac is the empty string.
// In this case, display the nativePath, which will be "/".
if (label == "")
label = item.nativePath;
if (!item.isDirectory && !showExtensions)
{
var index:int = label.lastIndexOf(".");
if (index != -1)
label = label.substring(0, index);
}
return label;
}
/**
* @private
*/
public function findIndex(nativePath:String):int
{
if (!nativePath)
return -1;
if (fileSystemIsCaseInsensitive())
nativePath = nativePath.toLowerCase();
var value:* = nativePathToIndexMap[nativePath];
return value === undefined ? -1 : int(value);
}
/**
* @private
*/
public function findItem(nativePath:String):File
{
var index:int = findIndex(nativePath);
if (index == -1)
return null;
return itemArray[index];
}
/**
* @private
* This method is called whenever something happens
* that affects which items are displayed by the
* control, or the order in which they are displayed.
*/
mx_internal function itemsChanged():void
{
// These two data structures are now invalid, so free them.
// They will be rebuilt the next time they are needed.
_itemArray = null;
_nativePathToIndexMap = null;
}
/**
* @private
*/
private function rebuildEnumerationInfo():void
{
_itemArray = [];
_nativePathToIndexMap = {};
enumerateItems(addItemToEnumerationInfo);
}
/**
* @private
*/
private function addItemToEnumerationInfo(index:int, item:File):void
{
var nativePath:String = item.nativePath;
if (fileSystemIsCaseInsensitive())
nativePath = nativePath.toLowerCase();
_itemArray.push(item);
_nativePathToIndexMap[nativePath] = index;
}
/**
* @private
*/
private function enumerateItems(itemCallback:Function):int
{
return enumerate(ArrayCollection(owner.dataProvider),
0, itemCallback);
}
/**
* @private
*/
private function enumerate(items:ArrayCollection, index:int,
itemCallback:Function):int
{
var n:int = items.length;
for (var i:int = 0; i < n; i++)
{
var item:File = File(items.getItemAt(i));
itemCallback(index, item);
index++;
if (hierarchical && item.isDirectory && owner.isItemOpen(item))
{
var childItems:ArrayCollection =
owner.dataDescriptor.getChildren(item);
index = enumerate(childItems, index, itemCallback);
}
}
return index;
}
/**
* @private
*/
public function navigateDown():void
{
if (canNavigateDown)
navigateTo(File(owner.selectedItem));
}
/**
* @private
*/
public function navigateUp():void
{
if (canNavigateUp)
navigateTo(directory.parent ? directory.parent : COMPUTER);
}
/**
* @private
*/
public function navigateBack(index:int = 0):void
{
if (canNavigateBack)
navigateBy(-1 - index);
}
/**
* @private
*/
public function navigateForward(index:int = 0):void
{
if (canNavigateForward)
navigateBy(1 + index)
}
/**
* @private
*/
private function navigateBy(n:int):void
{
historyIndex += n;
if (historyIndex < 0)
historyIndex = 0;
else if (historyIndex > history.length - 1)
historyIndex = history.length - 1;
setDirectory(history[historyIndex]);
owner.dispatchEvent(new Event("historyChanged"));
}
/**
* @private
*/
public function navigateTo(directory:File):void
{
setDirectory(directory);
pushHistory(directory);
}
/**
* @private
*/
public function refresh():void
{
var openPaths:Array /* of String */
var selectedPaths:Array /* of String */;
var firstVisiblePath:String;
var oldHorizontalScrollPosition:int;
if (hierarchical)
openPaths = getOpenPaths();
selectedPaths = getSelectedPaths();
firstVisiblePath = getFirstVisiblePath();
oldHorizontalScrollPosition = owner.horizontalScrollPosition;
fill();
// Tree must be revalidated after its dataProvider
// changes for expandItem() to work.
if (hierarchical)
owner.validateNow();
if (hierarchical)
setOpenPaths(openPaths);
setSelectedPaths(selectedPaths);
if (setFirstVisiblePath(firstVisiblePath))
owner.horizontalScrollPosition = oldHorizontalScrollPosition;
}
/**
* @private
*/
private function getOpenPaths():Array /* of String */
{
var openPaths:Array /* of String */ = [];
var n:int = owner.openItems.length;
for (var i:int = 0; i < n; i++)
{
openPaths.push(File(owner.openItems[i]).nativePath);
}
return openPaths;
}
/**
* @private
* Returns an Array of nativePath Strings for the selected items.
* This method is called by refresh() before repopulating the control.
*/
private function getSelectedPaths():Array /* of String */
{
var selectedPaths:Array /* of String */ = [];
var n:int = owner.selectedItems.length;
for (var i:int = 0; i < n; i++)
{
selectedPaths.push(File(owner.selectedItems[i]).nativePath);
}
return selectedPaths;
}
/**
* @private
* Returns the nativePath of the first visible item.
* This method is called by refresh() before repopulating the control.
*/
private function getFirstVisiblePath():String
{
if (owner.dataProvider == null || owner.dataProvider.length == 0)
return null;
var index:int = owner.verticalScrollPosition;
var item:File = itemArray[index];
return item ? item.nativePath : null;
}
/**
* @private
*/
private function setOpenPaths(openPaths:Array /* of String */):void
{
var n:int = openPaths.length;
for (var i:int = 0; i < n; i++)
{
owner.openSubdirectory(openPaths[i]);
}
}
/**
* @private
* Selects items whose nativePaths are in the specified Array.
* This method is called by refresh() after repopulating the control.
*/
private function setSelectedPaths(selectedPaths:Array /* of String */):void
{
var indices:Array /* of int */ = [];
var n:int = selectedPaths.length;
for (var i:int = 0; i < n; i++)
{
var path:String = selectedPaths[i];
var index:int = findIndex(path);
if (index != -1)
indices.push(index);
}
owner.selectedIndices = indices;
}
/**
* @private
* Scrolls the list to the item with the specified nativePath.
* This method is by refresh() after repopulating the control.
*/
private function setFirstVisiblePath(path:String):Boolean
{
if (path == null)
return false;
var index:int = findIndex(path);
if (index == -1)
return false;
owner.verticalScrollPosition = index;
return true;
}
/**
* @private
*/
public function clear():void
{
owner.dataProvider = null;
itemsChanged();
}
/**
* @private
*/
public function resetHistory(directory:File):void
{
history = [ directory ];
historyIndex = 0;
owner.dispatchEvent(new Event("historyChanged"));
}
/**
* @private
*/
private function pushHistory(directory:File):void
{
historyIndex++;
history.splice(historyIndex);
history.push(directory);
owner.dispatchEvent(new Event("historyChanged"));
}
/**
* @private
* Returns an Array of File objects
* representing the path to the specified directory.
* The first File represents a root directory.
* The last File represents the specified file's parent directory.
*/
public function getParentChain(file:File):Array
{
if (!file)
return [];
var a:Array = [];
for (var f:File = file; f != null; f = f.parent)
{
a.unshift(f);
}
return a;
}
/**
* @private
* Dispatches a cancelable "directoryChanging" event
* and returns true if it wasn't canceled.
*/
mx_internal function dispatchDirectoryChangingEvent(newDirectory:File):Boolean
{
var event:FileEvent =
new FileEvent(FileEvent.DIRECTORY_CHANGING, false, true);
event.file = newDirectory;
owner.dispatchEvent(event);
return !event.isDefaultPrevented();
}
/**
* @private
* Dispatches a "fileChoose" event.
*/
mx_internal function dispatchFileChooseEvent(file:File):void
{
var event:FileEvent = new FileEvent(FileEvent.FILE_CHOOSE);
event.file = file;
owner.dispatchEvent(event);
}
/**
* @private
*/
private function getBackDirectory():File
{
return historyIndex == 0 ?
null :
history[historyIndex - 1];
}
/**
* @private
*/
private function getForwardDirectory():File
{
return historyIndex == history.length - 1 ?
null :
history[historyIndex + 1];
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
private function updateCompleteHandler(event:FlexEvent):void
{
if (pendingOpenPaths != null)
{
setOpenPaths(pendingOpenPaths);
pendingOpenPaths = null;
}
if (pendingSelectedPaths != null)
{
setSelectedPaths(pendingSelectedPaths);
pendingSelectedPaths = null;
}
}
/**
* @private
*/
public function itemDoubleClickHandler(event:ListEvent):void
{
var selectedFile:File = File(owner.selectedItem);
if (selectedFile.isDirectory)
{
if (dispatchDirectoryChangingEvent(selectedFile))
navigateDown();
}
else
{
dispatchFileChooseEvent(selectedFile);
}
}
/**
* @private
*/
public function handleKeyDown(event:KeyboardEvent):Boolean
{
switch (event.keyCode)
{
case Keyboard.ENTER:
{
var selectedFile:File = File(owner.selectedItem);
if (canNavigateDown &&
dispatchDirectoryChangingEvent(selectedFile))
{
navigateDown();
}
else
{
dispatchFileChooseEvent(selectedFile);
}
return true;
}
case Keyboard.BACKSPACE:
{
if (canNavigateUp &&
dispatchDirectoryChangingEvent(directory.parent))
{
navigateUp();
}
return true;
}
}
return false;
}
}
}