[Pkg-mozext-commits] [tabmixplus] 69/147: Add new option to open unloaded tabs from bookmarks and history
David Prévot
taffit at moszumanska.debian.org
Sat Aug 5 15:27:37 UTC 2017
This is an automated email from the git hooks/post-receive script.
taffit pushed a commit to branch master
in repository tabmixplus.
commit 242dc78616da6e0ed45d929521e00d204cc58142
Author: onemen <tabmix.onemen at gmail.com>
Date: Wed Dec 21 23:47:15 2016 +0200
Add new option to open unloaded tabs from bookmarks and history
---
chrome/content/links/userInterface.js | 14 +++-
chrome/content/places/places.js | 136 +++++++++++++++++++++++++++-------
chrome/content/preferences/events.js | 62 +++++++++++++++-
chrome/content/preferences/events.xul | 49 ++++++++++--
chrome/content/preferences/links.js | 2 +
chrome/content/tabmix.js | 1 +
chrome/locale/en-US/pref-tabmix.dtd | 4 +
defaults/preferences/tabmix.js | 2 +
modules/TabRestoreQueue.jsm | 92 +++++++++++++++++++++++
modules/TabmixSvc.jsm | 2 +
10 files changed, 329 insertions(+), 35 deletions(-)
diff --git a/chrome/content/links/userInterface.js b/chrome/content/links/userInterface.js
index 506c8f0..163f536 100644
--- a/chrome/content/links/userInterface.js
+++ b/chrome/content/links/userInterface.js
@@ -324,6 +324,18 @@ Tabmix.restoreTabState = function TMP_restoreTabState(aTab) {
aTab.removeAttribute("maxwidth");
};
+Tabmix.isPendingTab = function(aTab) {
+ const pending = aTab.hasAttribute("pending") || aTab.hasAttribute("tabmix_pending");
+ if (pending) {
+ if (TMP_Places.restoringTabs.indexOf(aTab) > -1) {
+ // don't show tab as pending when bookmarks restore_on_demand is false
+ return TMP_Places.bookmarksOnDemand;
+ }
+ return true;
+ }
+ return false;
+};
+
Tabmix.setTabStyle = function(aTab, boldChanged) {
if (!aTab)
return;
@@ -331,7 +343,7 @@ Tabmix.setTabStyle = function(aTab, boldChanged) {
let isSelected = aTab.getAttribute(TabmixSvc.selectedAtt) == "true";
// if pending tab is blank we don't style it as unload or unread
if (!isSelected && Tabmix.prefs.getBoolPref("unloadedTab") &&
- (aTab.hasAttribute("pending") || aTab.hasAttribute("tabmix_pending"))) {
+ this.isPendingTab(aTab)) {
style = aTab.pinned || aTab.hasAttribute("visited") ||
TMP_SessionStore.isBlankPendingTab(aTab) ? "other" : "unloaded";
} else if (!isSelected && Tabmix.prefs.getBoolPref("unreadTab") &&
diff --git a/chrome/content/places/places.js b/chrome/content/places/places.js
index 896c6a3..c449393 100644
--- a/chrome/content/places/places.js
+++ b/chrome/content/places/places.js
@@ -21,6 +21,9 @@ var TMP_Places = {
window.removeEventListener("unload", this, false);
this.deinit();
break;
+ case "SSTabRestored":
+ this.updateRestoringTabsList(aEvent.target);
+ break;
}
},
@@ -33,6 +36,7 @@ var TMP_Places = {
// use tab label for bookmark name when user renamed the tab
// PlacesCommandHook exist on browser window
if ("PlacesCommandHook" in window) {
+ gBrowser.tabContainer.addEventListener("SSTabRestored", this);
if (Tabmix.isVersion(400)) {
if (!Tabmix.originalFunctions.placesBookmarkPage) {
Tabmix.originalFunctions.placesBookmarkPage = PlacesCommandHook.bookmarkPage;
@@ -87,6 +91,10 @@ var TMP_Places = {
},
deinit: function TMP_PC_deinit() {
+ if ("gBrowser" in window) {
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", this);
+ this.restoringTabs = [];
+ }
this.stopObserver();
},
@@ -226,10 +234,8 @@ var TMP_Places = {
// locked and protected tabs open bookmark after those tabs
// fixed: focus the first tab if "extensions.tabmix.openTabNext" is true
// fixed: remove "selected" and "tabmix_selectedID" from reuse tab
- //
- // TODO - try to use sessionStore to add many tabs
openGroup: function TMP_PC_openGroup(bmGroup, bmIds, aWhere) {
- var tabs = gBrowser.visibleTabs;
+ var openTabs = gBrowser.visibleTabs;
var doReplace = (/^tab/).test(aWhere) ? false :
Tabmix.prefs.getBoolPref("loadBookmarksAndReplace");
@@ -241,8 +247,8 @@ var TMP_Places = {
// catch tab for reuse
var aTab, reuseTabs = [], removeTabs = [], i;
var tabIsBlank, canReplace;
- for (i = 0; i < tabs.length; i++) {
- aTab = tabs[i];
+ for (i = 0; i < openTabs.length; i++) {
+ aTab = openTabs[i];
tabIsBlank = gBrowser.isBlankNotBusyTab(aTab);
// don't reuse collapsed tab if width fitTitle is set
canReplace = (doReplace && !aTab.hasAttribute("locked") &&
@@ -258,32 +264,33 @@ var TMP_Places = {
}
var tabToSelect = null;
- var prevTab = (!doReplace && openTabNext && gBrowser.mCurrentTab._tPos < tabs.length - 1) ?
+ var prevTab = (!doReplace && openTabNext && gBrowser.mCurrentTab._tPos < openTabs.length - 1) ?
gBrowser.mCurrentTab : Tabmix.visibleTabs.last;
var tabPos, index;
var multiple = bmGroup.length > 1;
+ let tabs = [], tabsData = [];
for (i = 0; i < bmGroup.length; i++) {
let url = bmGroup[i];
- try { // bug 300911
- if (i < reuseTabs.length) {
- aTab = reuseTabs[i];
- let browser = gBrowser.getBrowserForTab(aTab);
- browser.userTypedValue = url;
- browser.loadURI(url);
- // setTabTitle will call TabmixTabbar.updateScrollStatus for us
- aTab.collapsed = false;
- // reset visited & tabmix_selectedID attribute
- if (!aTab.selected) {
- aTab.removeAttribute("visited");
- aTab.removeAttribute("tabmix_selectedID");
- } else
- aTab.setAttribute("reloadcurrent", true);
- } else {
- aTab = gBrowser.addTab(url, {skipAnimation: multiple, dontMove: true});
- }
-
- this.setTabTitle(aTab, url, bmIds[i]);
- } catch (er) { }
+ if (i < reuseTabs.length) {
+ aTab = reuseTabs[i];
+ this.resetRestoreState(aTab);
+ aTab.collapsed = false;
+ // reset visited & tabmix_selectedID attribute
+ if (!aTab.selected) {
+ aTab.removeAttribute("visited");
+ aTab.removeAttribute("tabmix_selectedID");
+ } else
+ aTab.setAttribute("reloadcurrent", true);
+ } else {
+ aTab = gBrowser.addTab("about:blank", {
+ skipAnimation: multiple,
+ dontMove: true,
+ forceNotRemote: true,
+ });
+ }
+ this.setTabTitle(aTab, url, bmIds[i]);
+ tabs.push(aTab);
+ tabsData.push({entries: [{url: url, title: aTab.label}], index: 0});
if (!tabToSelect)
tabToSelect = aTab;
@@ -319,6 +326,83 @@ var TMP_Places = {
while (removeTabs.length > 0) {
gBrowser.removeTab(removeTabs.pop());
}
+
+ // we use two preferences to control this feature:
+ // load_tabs_progressively - type int:
+ // When the number of tabs to load is more than the number in the preference
+ // we use SessionStore to restore each tab otherwise we use browser.loadURI.
+ //
+ // restore_on_demand - type int:
+ // when the number of tabs to load exceed the number in the preference we
+ // instruct SessionStore to use restore on demand for the current set of tabs.
+ const tabCount = this.restoringTabs.length + tabs.length;
+ const [loadProgressively, restoreOnDemand] = this.getPreferences(tabCount);
+ if (loadProgressively) {
+ this.restoreTabs(tabs, tabsData, this.bookmarksOnDemand || restoreOnDemand);
+ } else {
+ this.loadTabs(tabs, tabsData, bmIds);
+ }
+ },
+
+ getPreferences(tabCount) {
+ // negative value indicate that the feature is disabled
+ const progressively = Tabmix.prefs.getIntPref("load_tabs_progressively");
+ if (progressively < 0) {
+ return [false, false];
+ }
+ let onDemand = Tabmix.prefs.getIntPref("restore_on_demand");
+ if (onDemand < 0) {
+ return [tabCount > progressively, false];
+ }
+ if (onDemand < progressively) {
+ Tabmix.prefs.setIntPref("restore_on_demand", progressively);
+ onDemand = progressively;
+ }
+ return [tabCount > progressively, tabCount > onDemand];
+ },
+
+ restoreTabs: function(tabs, tabsData, restoreOnDemand) {
+ this.restoringTabs.push(...tabs);
+ this.bookmarksOnDemand = restoreOnDemand;
+ let fnName = Tabmix.isVersion(280) ? "restoreTabs" :
+ "restoreHistoryPrecursor";
+ TabmixSvc.SessionStore[fnName](window, tabs, tabsData, 0);
+ },
+
+ loadTabs: function(tabs, tabsData, ids) {
+ for (let tab of tabs) {
+ const url = tabsData.shift().entries[0].url;
+ const browser = tab.linkedBrowser;
+ try {
+ browser.stop();
+ browser.userTypedValue = url;
+ browser.loadURI(url);
+ this.setTabTitle(tab, url, ids.shift());
+ } catch (ex) { }
+ }
+ },
+
+ bookmarksOnDemand: false,
+ restoringTabs: [],
+
+ resetRestoreState: function(tab) {
+ if (tab.linkedBrowser.__SS_restoreState) {
+ TabmixSvc.SessionStore._resetTabRestoringState(tab);
+ }
+ this.updateRestoringTabsList(tab);
+ },
+
+ updateRestoringTabsList: function(tab) {
+ if (!this.restoringTabs.length && !this.bookmarksOnDemand) {
+ return;
+ }
+ let index = this.restoringTabs.indexOf(tab);
+ if (index > -1) {
+ this.restoringTabs.splice(index, 1);
+ }
+ if (!this.restoringTabs.length) {
+ this.bookmarksOnDemand = false;
+ }
},
setTabTitle: function TMP_PC_setTabTitle(aTab, aUrl, aID) {
diff --git a/chrome/content/preferences/events.js b/chrome/content/preferences/events.js
index 81afa46..c87fbd2 100644
--- a/chrome/content/preferences/events.js
+++ b/chrome/content/preferences/events.js
@@ -50,6 +50,7 @@ var gEventsPane = {
}
this.alignTabOpeningBoxes();
+ this.loadProgressively.init();
gPrefWindow.initPane("paneEvents");
},
@@ -140,5 +141,64 @@ var gEventsPane = {
gMenuPane.editSlideShowKey();
else
$("paneMenu").setAttribute("editSlideShowKey", true);
- }
+ },
+
+ loadProgressively: {
+ init() {
+ this._init("pref_loadProgressively");
+ this._init("pref_restoreOnDemand");
+ this.syncToPref();
+ this.setOnDemandState();
+ },
+
+ _init(id) {
+ const preference = $(id);
+ if (preference.value == 0) {
+ preference.value = 1;
+ }
+ if (preference.hasAttribute("notChecked")) {
+ preference.setAttribute("notChecked", -Math.abs(preference.value));
+ }
+ },
+
+ syncToCheckBox: function(item) {
+ let preference = $(item.getAttribute("preference"));
+ return preference.value > -1;
+ },
+
+ syncFromCheckBox: function(item) {
+ let preference = $(item.getAttribute("preference"));
+ let control = $(item.getAttribute("control"));
+ if (preference.hasAttribute("notChecked")) {
+ preference.setAttribute("notChecked", -Math.abs(preference.value));
+ }
+ control.disabled = !item.checked;
+ this.setOnDemandState();
+ return -preference.value;
+ },
+
+ syncFromPref(item) {
+ const preference = $(item.getAttribute("preference"));
+ return Math.abs(preference.value);
+ },
+
+ syncToPref() {
+ const onDemand = $("restoreOnDemand");
+ const item = $("loadProgressively");
+ onDemand.min = item.valueNumber;
+ onDemand._enableDisableButtons();
+ const preference = $("pref_loadProgressively");
+ const restoreOnDemand = $("pref_restoreOnDemand");
+ if (Math.abs(preference.value) > Math.abs(restoreOnDemand.value)) {
+ const val = Math.abs(preference.value);
+ restoreOnDemand.value = $("chk_restoreOnDemand").checked ? val : -val;
+ }
+ },
+
+ setOnDemandState() {
+ const disabled = !$("chk_loadProgressively").checked ||
+ !$("chk_restoreOnDemand").checked;
+ gPrefWindow.setDisabled("restoreOnDemand", disabled);
+ },
+ },
};
diff --git a/chrome/content/preferences/events.xul b/chrome/content/preferences/events.xul
index dd7871a..1a392a7 100644
--- a/chrome/content/preferences/events.xul
+++ b/chrome/content/preferences/events.xul
@@ -31,6 +31,9 @@
<preference id="pref_openTabNextInverse" name="extensions.tabmix.openTabNextInverse" type="bool"/>
<preference id="pref_relatedAfterCurrent" name="browser.tabs.insertRelatedAfterCurrent" type="bool"/>
<preference id="pref_openDuplicateNext" name="extensions.tabmix.openDuplicateNext" type="bool"/>
+ <preference id="pref_loadProgressively" name="extensions.tabmix.load_tabs_progressively"
+ type="int" notChecked=""/>
+ <preference id="pref_restoreOnDemand" name="extensions.tabmix.restore_on_demand" type="int"/>
<preference id="pref_lockallTabs" name="extensions.tabmix.lockallTabs" type="bool"/>
<preference id="pref_lockAppTabs" name="extensions.tabmix.lockAppTabs" type="bool"/>
<preference id="pref_updateLockState" name="extensions.tabmix.updateOpenedTabsLockState"
@@ -167,14 +170,45 @@
preference="pref_openDuplicateNext"/>
</groupbox>
<groupbox flex="1">
+ <!-- Load Bookmarks/History progressively -->
+ <caption label="&openPlacesGroups.label;"/>
+ <hbox align="center">
+ <checkbox_tmp id="chk_loadProgressively" label="&loadTabsProgressively.label;, &moreThan.label;"
+ preference="pref_loadProgressively" control="loadProgressively"
+ onsyncfrompreference="return gEventsPane.loadProgressively.syncToCheckBox(this);"
+ onsynctopreference="return gEventsPane.loadProgressively.syncFromCheckBox(this);"/>
+ <textbox id="loadProgressively" maxlength="3" size="2" preference="pref_loadProgressively"
+ onsyncfrompreference="return gEventsPane.loadProgressively.syncFromPref(this);"
+ onsynctopreference="return gEventsPane.loadProgressively.syncToPref(this);"
+ observes="obs_loadProgressively"
+ type="number" min="1" maxwidth="36"/>
+ <label value="&tabs.label;"/>
+ </hbox>
+ <hbox align="center" class="indent">
+ <checkbox_tmp id="chk_restoreOnDemand" label="&restoreOnDemand.label;, &moreThan.label;"
+ preference="pref_restoreOnDemand" control="restoreOnDemand"
+ onsyncfrompreference="return gEventsPane.loadProgressively.syncToCheckBox(this);"
+ onsynctopreference="return gEventsPane.loadProgressively.syncFromCheckBox(this);"
+ observes="obs_loadProgressively"/>
+ <textbox id="restoreOnDemand" maxlength="3" size="2" preference="pref_restoreOnDemand"
+ onsyncfrompreference="return gEventsPane.loadProgressively.syncFromPref(this);"
+ type="number" min="1" maxwidth="36"/>
+ <label value="&tabs.label;" observes="obs_loadProgressively"/>
+ </hbox>
+ </groupbox>
+ <groupbox flex="1" orient="horizontal">
<caption label="&lockTabs.label;"/>
- <!-- Look All Tabs -->
- <checkbox_tmp id="lockallTabs" pack="start" label="&lockNewTabs.label;" preference="pref_lockallTabs"/>
- <!-- Look App Tabs -->
- <checkbox_tmp id="lockAppTabs" align="start" label="&lockAppTabs.label;" preference="pref_lockAppTabs"/>
- <spacer style="height: 1em;"/>
- <checkbox_tmp id="updateLockState" align="center" label="&updateLockState.label;"
- preference="pref_updateLockState"/>
+ <vbox flex="1" pack="end">
+ <!-- Look All Tabs -->
+ <checkbox_tmp id="lockallTabs" pack="start" label="&lockNewTabs.label;" preference="pref_lockallTabs"/>
+ <!-- Look App Tabs -->
+ <checkbox_tmp id="lockAppTabs" align="start" label="&lockAppTabs.label;" preference="pref_lockAppTabs"/>
+ </vbox>
+ <vbox flex="1" pack="end">
+ <!--<spacer style="height: 3px;"/>-->
+ <checkbox_tmp id="updateLockState" align="center" label="&updateLockState.label;"
+ preference="pref_updateLockState"/>
+ </vbox>
</groupbox>
</tabpanel>
<!-- ======================================================== -->
@@ -399,6 +433,7 @@
<broadcaster id="obs_closeOnMerge"/>
<broadcaster id="obs_ctrltab"/>
<broadcaster id="obs_showTabList"/>
+ <broadcaster id="obs_loadProgressively"/>
</broadcasterset>
<broadcasterset>
diff --git a/chrome/content/preferences/links.js b/chrome/content/preferences/links.js
index c0087f1..742457e 100644
--- a/chrome/content/preferences/links.js
+++ b/chrome/content/preferences/links.js
@@ -6,6 +6,8 @@ var gLinksPane = {
this.singleWindow($("singleWindow").checked);
this.externalLinkValue($("externalLink").checked);
+ gPrefWindow.setDisabled("obs_opentabforAllLinks", $("pref_opentabforLinks").value == 1);
+
gPrefWindow.initPane("paneLinks");
},
diff --git a/chrome/content/tabmix.js b/chrome/content/tabmix.js
index 2790128..e5c0eea 100644
--- a/chrome/content/tabmix.js
+++ b/chrome/content/tabmix.js
@@ -836,6 +836,7 @@ var TMP_eventListener = {
tab._tPosInGroup = TMP_TabView.getTabPosInCurrentGroup(tab);
TMP_LastTab.tabs = null;
TMP_LastTab.detachTab(tab);
+ TMP_Places.updateRestoringTabsList(tab);
var tabBar = gBrowser.tabContainer;
// if we close the 2nd tab and tabbar is hide when there is only one tab
diff --git a/chrome/locale/en-US/pref-tabmix.dtd b/chrome/locale/en-US/pref-tabmix.dtd
index e2b0997..4696a01 100644
--- a/chrome/locale/en-US/pref-tabmix.dtd
+++ b/chrome/locale/en-US/pref-tabmix.dtd
@@ -56,6 +56,10 @@
<!ENTITY openTabNextInverse.label "Change opening order">
<!ENTITY openTabNextInverse.tooltip "[a][3][2][1][b][c] -> [a][1][2][3][b][c]">
<!ENTITY openTabNextInverse.tooltip1 "Open new tab next to the tab last opened from the current tab (since it was last selected)">
+<!ENTITY loadTabsProgressively.label "Load tabs progressively">
+<!ENTITY restoreOnDemand.label "Don't load tabs until selected">
+<!ENTITY moreThan.label "when I open more than">
+<!ENTITY tabs.label "tabs">
<!ENTITY lockTabs.label "Lock tabs">
<!ENTITY lockNewTabs.label "Lock New tabs">
<!ENTITY lockAppTabs.label "Lock App tabs">
diff --git a/defaults/preferences/tabmix.js b/defaults/preferences/tabmix.js
index 5d0a375..7bbbc24 100644
--- a/defaults/preferences/tabmix.js
+++ b/defaults/preferences/tabmix.js
@@ -12,6 +12,8 @@ pref("extensions.tabmix.updateOpenedTabsLockState", true); // added 2012-12-29
pref("extensions.tabmix.singleWindow", false);
pref("extensions.tabmix.opentabfor.bookmarks", false);
+pref("extensions.tabmix.load_tabs_progressively", 9);
+pref("extensions.tabmix.restore_on_demand", 9);
// pref("extensions.tabmix.opentabfor.search", false); - replace with Firefox pref
pref("extensions.tabmix.opentabfor.history", false);
pref("extensions.tabmix.opentabfor.urlbar", false);
diff --git a/modules/TabRestoreQueue.jsm b/modules/TabRestoreQueue.jsm
new file mode 100644
index 0000000..24f20ad
--- /dev/null
+++ b/modules/TabRestoreQueue.jsm
@@ -0,0 +1,92 @@
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["TabRestoreQueue"];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "TabmixSvc", "resource://tabmixplus/TabmixSvc.jsm");
+
+let internal = {
+ tabmix: {
+ restoreOnDemand: function(restoreOnDemand, visible, tabToRestoreSoon) {
+ if (!visible.length) {
+ return restoreOnDemand;
+ }
+
+ const tab = tabToRestoreSoon || visible[0];
+ const win = tab.ownerDocument.defaultView;
+ const {restoringTabs, bookmarksOnDemand} = win.TMP_Places;
+ const index = restoringTabs.indexOf(tab);
+ if (index > -1) {
+ if (!tabToRestoreSoon && !bookmarksOnDemand) {
+ restoringTabs.splice(index, 1);
+ }
+ return bookmarksOnDemand;
+ }
+
+ // not our tab
+ return restoreOnDemand;
+ },
+ },
+
+ // Returns and removes the tab with the highest priority.
+ shift: function() {
+ let set;
+ let {priority, hidden, visible} = this.tabs;
+
+ let {restoreOnDemand, restorePinnedTabsOnDemand} = this.prefs;
+ let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
+ if (restorePinned && priority.length) {
+ set = priority;
+ } else if (!this.tabmix.restoreOnDemand(restoreOnDemand, visible)) {
+ if (visible.length) {
+ set = visible;
+ } else if (this.prefs.restoreHiddenTabs && hidden.length) {
+ set = hidden;
+ }
+ }
+
+ return set && set.shift();
+ },
+
+ willRestoreSoon: function(tab) {
+ let {priority, hidden, visible} = this.tabs;
+ let {restoreOnDemand, restorePinnedTabsOnDemand,
+ restoreHiddenTabs} = this.prefs;
+ let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
+ let candidateSet = [];
+
+ if (restorePinned && priority.length)
+ candidateSet.push(...priority);
+
+ if (!this.tabmix.restoreOnDemand(restoreOnDemand, visible, tab)) {
+ if (visible.length)
+ candidateSet.push(...visible);
+
+ if (restoreHiddenTabs && hidden.length)
+ candidateSet.push(...hidden);
+ }
+
+ return candidateSet.indexOf(tab) > -1;
+ },
+};
+
+this.TabRestoreQueue = {
+ init: function() {
+ const global = {};
+ const tabRestoreQueue = TabmixSvc.SessionStoreGlobal.TabRestoreQueue;
+ global.TabRestoreQueue = tabRestoreQueue;
+ for (let key of Object.keys(internal)) {
+ if (typeof internal[key] == "function") {
+ global.TabRestoreQueue[key] = internal[key].bind(tabRestoreQueue);
+ } else {
+ global.TabRestoreQueue[key] = internal[key];
+ }
+ }
+ },
+};
+
+this.TabRestoreQueue.init();
diff --git a/modules/TabmixSvc.jsm b/modules/TabmixSvc.jsm
index 3145b87..f26d631 100644
--- a/modules/TabmixSvc.jsm
+++ b/modules/TabmixSvc.jsm
@@ -199,6 +199,8 @@ this.TabmixSvc = {
let tmp = {};
Cu.import("resource://tabmixplus/DynamicRules.jsm", tmp);
tmp.DynamicRules.init(aWindow);
+
+ Cu.import("resource://tabmixplus/TabRestoreQueue.jsm", {});
},
addMissingPrefs: function() {
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mozext/tabmixplus.git
More information about the Pkg-mozext-commits
mailing list