blob: 785bd756f0d846967f5461a62f16d93f4539c7d4 [file] [log] [blame]
/*
* Copyright (C) 2014-2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WI.FolderizedTreeElement = class FolderizedTreeElement extends WI.GeneralTreeElement
{
constructor(classNames, title, subtitle, representedObject)
{
super(classNames, title, subtitle, representedObject);
this.shouldRefreshChildren = true;
this._folderExpandedSettingMap = new Map;
this._folderSettingsKey = "";
this._folderTypeMap = new Map;
this._folderizeSettingsMap = new Map;
this._groupedIntoFolders = false;
this._clearNewChildQueue();
}
// Public
get groupedIntoFolders()
{
return this._groupedIntoFolders;
}
set folderSettingsKey(x)
{
this._folderSettingsKey = x;
}
registerFolderizeSettings(type, displayName, representedObject, treeElementConstructor)
{
console.assert(type);
console.assert(displayName || displayName === null);
console.assert(representedObject);
console.assert(typeof treeElementConstructor === "function");
let settings = {
type,
displayName,
topLevel: displayName === null,
representedObject,
treeElementConstructor,
};
this._folderizeSettingsMap.set(type, settings);
}
// Overrides from TreeElement (Private).
removeChildren()
{
super.removeChildren();
this._clearNewChildQueue();
for (var folder of this._folderTypeMap.values())
folder.removeChildren();
this._folderExpandedSettingMap.clear();
this._folderTypeMap.clear();
this._groupedIntoFolders = false;
}
// Protected
addChildForRepresentedObject(representedObject)
{
var settings = this._settingsForRepresentedObject(representedObject);
console.assert(settings);
if (!settings) {
console.error("No settings for represented object", representedObject);
return;
}
if (!this.treeOutline) {
// Just mark as needing to update to avoid doing work that might not be needed.
this.shouldRefreshChildren = true;
return;
}
var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
if (!childTreeElement)
childTreeElement = new settings.treeElementConstructor(representedObject);
this._addTreeElement(childTreeElement);
}
addRepresentedObjectToNewChildQueue(representedObject)
{
// This queue reduces flashing as resources load and change folders when their type becomes known.
this._newChildQueue.push(representedObject);
if (!this._newChildQueueTimeoutIdentifier)
this._newChildQueueTimeoutIdentifier = setTimeout(this._populateFromNewChildQueue.bind(this), WI.FolderizedTreeElement.NewChildQueueUpdateInterval);
}
removeChildForRepresentedObject(representedObject)
{
this._removeRepresentedObjectFromNewChildQueue(representedObject);
this.updateParentStatus();
if (!this.treeOutline) {
// Just mark as needing to update to avoid doing work that might not be needed.
this.shouldRefreshChildren = true;
return;
}
// Find the tree element for the frame by using getCachedTreeElement
// to only get the item if it has been created already.
var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject);
if (!childTreeElement || !childTreeElement.parent)
return;
this._removeTreeElement(childTreeElement);
}
compareChildTreeElements(a, b)
{
return this._compareTreeElementsByMainTitle(a, b);
}
updateParentStatus()
{
let hasChildren = false;
for (let settings of this._folderizeSettingsMap.values()) {
if (settings.representedObject.items.size) {
hasChildren = true;
break;
}
}
this.hasChildren = hasChildren;
if (!this.hasChildren)
this.removeChildren();
}
prepareToPopulate()
{
if (!this._groupedIntoFolders && this._shouldGroupIntoFolders()) {
this._groupedIntoFolders = true;
return true;
}
return false;
}
// Private
_clearNewChildQueue()
{
this._newChildQueue = [];
if (this._newChildQueueTimeoutIdentifier) {
clearTimeout(this._newChildQueueTimeoutIdentifier);
this._newChildQueueTimeoutIdentifier = null;
}
}
_populateFromNewChildQueue()
{
if (!this.children.length) {
this.updateParentStatus();
this.shouldRefreshChildren = true;
return;
}
if (this.prepareToPopulate()) {
// Will now folderize, repopulate children.
this._clearNewChildQueue();
this.shouldRefreshChildren = true;
return;
}
for (var i = 0; i < this._newChildQueue.length; ++i)
this.addChildForRepresentedObject(this._newChildQueue[i]);
this._clearNewChildQueue();
}
_removeRepresentedObjectFromNewChildQueue(representedObject)
{
this._newChildQueue.remove(representedObject);
}
_addTreeElement(childTreeElement)
{
console.assert(childTreeElement);
if (!childTreeElement)
return;
var wasSelected = childTreeElement.selected;
this._removeTreeElement(childTreeElement, true, true);
var parentTreeElement = this._parentTreeElementForRepresentedObject(childTreeElement.representedObject);
if (parentTreeElement !== this && !parentTreeElement.parent)
this._insertFolderTreeElement(parentTreeElement);
this._insertChildTreeElement(parentTreeElement, childTreeElement);
if (wasSelected)
childTreeElement.revealAndSelect(true, false, true, true);
}
_compareTreeElementsByMainTitle(a, b)
{
// Folders before anything.
let aIsFolder = a instanceof WI.FolderTreeElement;
let bIsFolder = b instanceof WI.FolderTreeElement;
if (aIsFolder && !bIsFolder)
return -1;
if (bIsFolder && !aIsFolder)
return 1;
// Then sort by title.
return a.mainTitle.extendedLocaleCompare(b.mainTitle);
}
_insertFolderTreeElement(folderTreeElement)
{
console.assert(this._groupedIntoFolders);
console.assert(!folderTreeElement.parent);
this.insertChild(folderTreeElement, insertionIndexForObjectInListSortedByFunction(folderTreeElement, this.children, this._compareTreeElementsByMainTitle));
}
_insertChildTreeElement(parentTreeElement, childTreeElement)
{
console.assert(!childTreeElement.parent);
parentTreeElement.insertChild(childTreeElement, insertionIndexForObjectInListSortedByFunction(childTreeElement, parentTreeElement.children, this.compareChildTreeElements.bind(this)));
}
_removeTreeElement(childTreeElement, suppressOnDeselect, suppressSelectSibling)
{
var oldParent = childTreeElement.parent;
if (!oldParent)
return;
oldParent.removeChild(childTreeElement, suppressOnDeselect, suppressSelectSibling);
if (oldParent === this)
return;
console.assert(oldParent instanceof WI.FolderTreeElement);
if (!(oldParent instanceof WI.FolderTreeElement))
return;
// Remove the old parent folder if it is now empty.
if (!oldParent.children.length)
oldParent.parent.removeChild(oldParent);
}
_parentTreeElementForRepresentedObject(representedObject)
{
if (!this._groupedIntoFolders)
return this;
console.assert(this._folderSettingsKey !== "");
function createFolderTreeElement(settings)
{
let folderTreeElement = new WI.FolderTreeElement(settings.displayName, settings.representedObject);
let folderExpandedSetting = new WI.Setting(settings.type + "-folder-expanded-" + this._folderSettingsKey, false);
this._folderExpandedSettingMap.set(folderTreeElement, folderExpandedSetting);
if (folderExpandedSetting.value)
folderTreeElement.expand();
folderTreeElement.onexpand = this._folderTreeElementExpandedStateChange.bind(this);
folderTreeElement.oncollapse = this._folderTreeElementExpandedStateChange.bind(this);
return folderTreeElement;
}
var settings = this._settingsForRepresentedObject(representedObject);
if (!settings) {
console.error("Unknown representedObject", representedObject);
return this;
}
if (settings.topLevel)
return this;
var folder = this._folderTypeMap.get(settings.type);
if (folder)
return folder;
folder = createFolderTreeElement.call(this, settings);
this._folderTypeMap.set(settings.type, folder);
return folder;
}
_folderTreeElementExpandedStateChange(folderTreeElement)
{
let expandedSetting = this._folderExpandedSettingMap.get(folderTreeElement);
console.assert(expandedSetting, "No expanded setting for folderTreeElement", folderTreeElement);
if (!expandedSetting)
return;
expandedSetting.value = folderTreeElement.expanded;
}
_settingsForRepresentedObject(representedObject)
{
for (let settings of this._folderizeSettingsMap.values()) {
if (settings.representedObject.objectIsRequiredType(representedObject))
return settings;
}
return null;
}
_shouldGroupIntoFolders()
{
// Already grouped into folders, keep it that way.
if (this._groupedIntoFolders)
return true;
// Child objects are grouped into folders if one of two thresholds are met:
// 1) Once the number of medium categories passes NumberOfMediumCategoriesThreshold.
// 2) When there is a category that passes LargeChildCountThreshold and there are
// any child objects in another category.
// Folders are avoided when there is only one category or most categories are small.
var numberOfSmallCategories = 0;
var numberOfMediumCategories = 0;
var foundLargeCategory = false;
function pushCategory(childCount)
{
if (!childCount)
return false;
// If this type has any resources and there is a known large category, make folders.
if (foundLargeCategory)
return true;
// If there are lots of this resource type, then count it as a large category.
if (childCount >= WI.FolderizedTreeElement.LargeChildCountThreshold) {
// If we already have other resources in other small or medium categories, make folders.
if (numberOfSmallCategories || numberOfMediumCategories)
return true;
foundLargeCategory = true;
return false;
}
// Check if this is a medium category.
if (childCount >= WI.FolderizedTreeElement.MediumChildCountThreshold) {
// If this is the medium category that puts us over the maximum allowed, make folders.
return ++numberOfMediumCategories >= WI.FolderizedTreeElement.NumberOfMediumCategoriesThreshold;
}
// This is a small category.
++numberOfSmallCategories;
return false;
}
// Iterate over all the available child object types.
for (var settings of this._folderizeSettingsMap.values()) {
if (pushCategory(settings.representedObject.items.size))
return true;
}
return false;
}
};
WI.FolderizedTreeElement.MediumChildCountThreshold = 5;
WI.FolderizedTreeElement.LargeChildCountThreshold = 15;
WI.FolderizedTreeElement.NumberOfMediumCategoriesThreshold = 2;
WI.FolderizedTreeElement.NewChildQueueUpdateInterval = 500;