switch to scrolling GenericDisplay, remove menus from apps

Temporarily removed search-by-menu from apps as well, will readd
later as a ShellApp feature.

https://bugzilla.gnome.org/show_bug.cgi?id=600734
This commit is contained in:
Colin Walters 2009-11-03 18:36:44 -05:00
parent 1626e8c09a
commit d51384fb86
5 changed files with 86 additions and 328 deletions

View File

@ -166,15 +166,15 @@ Signals.addSignalMethods(MenuItem.prototype);
* showPrefs - a boolean indicating if this AppDisplay should contain preference
* applets, rather than applications
*/
function AppDisplay(showPrefs) {
this._init(showPrefs);
function AppDisplay(showPrefs, flags) {
this._init(showPrefs, flags);
}
AppDisplay.prototype = {
__proto__: GenericDisplay.GenericDisplay.prototype,
_init : function(showPrefs) {
GenericDisplay.GenericDisplay.prototype._init.call(this);
_init : function(showPrefs, flags) {
GenericDisplay.GenericDisplay.prototype._init.call(this, flags);
this._showPrefs = showPrefs;
@ -190,156 +190,10 @@ AppDisplay.prototype = {
this._appsStale = true;
this._redisplay(GenericDisplay.RedisplayFlags.NONE);
}));
this._focusInMenus = true;
this._activeMenuIndex = -1;
this._activeMenu = null;
this._activeMenuApps = null;
this._menuDisplay = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
spacing: MENU_SPACING
});
this.connect('expanded', Lang.bind(this, function (self) {
this._filterReset();
}));
this._filterReset();
},
moveRight: function() {
if (this._expanded && this._focusInMenu) {
this._focusInMenu = false;
this._activeMenu.setState(MENU_ENTERED);
this.selectFirstItem();
}
},
moveLeft: function() {
if (this._expanded && !this._focusInMenu) {
this._activeMenu.setState(MENU_SELECTED);
this.unsetSelected();
this._focusInMenu = true;
}
},
// Override genericDisplay.js
getNavigationArea: function() {
return this._menuDisplay;
},
selectUp: function() {
if (!(this._expanded && this._focusInMenu))
return GenericDisplay.GenericDisplay.prototype.selectUp.call(this);
this._selectMenuIndex(this._activeMenuIndex - 1);
return true;
},
selectDown: function() {
if (!(this._expanded && this._focusInMenu))
return GenericDisplay.GenericDisplay.prototype.selectDown.call(this);
this._selectMenuIndex(this._activeMenuIndex+1);
return true;
},
setSearch: function(text) {
let lowertext = text.toLowerCase();
if (lowertext == this._search)
return;
// We prepare menu matches up-front, so that we don't
// need to go over all menu items for each application
// and then get all applications for a matching menu
// to see if a particular application passed to
// _isInfoMatching() is a match.
let terms = lowertext.split(/\s+/);
this._menuSearchAppMatches = {};
for (let i = 0; i < terms.length; i++) {
let term = terms[i];
this._menuSearchAppMatches[term] = {};
for (let j = 0; j < this._menus.length; j++) {
let menuItem = this._menus[j];
// Match only on the beginning of the words in category names,
// because otherwise it introduces unnecessary noise in the results.
if (menuItem.name.toLowerCase().indexOf(term) == 0 ||
menuItem.name.toLowerCase().indexOf(" " + term) > 0) {
let menuApps = this._appSystem.get_applications_for_menu(menuItem.id);
for (let k = 0; k < menuApps.length; k++) {
let menuApp = menuApps[k];
this._menuSearchAppMatches[term][menuApp.get_id()] = true;
}
}
}
}
GenericDisplay.GenericDisplay.prototype.setSearch.call(this, text);
},
// Protected overrides
_filterActive: function() {
// We always have a filter now since a menu must be selected
return true;
},
_filterReset: function() {
GenericDisplay.GenericDisplay.prototype._filterReset.call(this);
this._selectMenuIndex(0);
},
//// Private ////
_emitStateChange: function() {
this.emit('state-changed');
},
_selectMenuIndex: function(index) {
if (index < 0 || index >= this._menus.length)
return;
this._menuDisplays[index].setState(MENU_SELECTED);
},
_getMostUsed: function() {
let context = "";
let usage = Shell.AppUsage.get_default();
return usage.get_most_used(context, 30);
},
_addMenuItem: function(name, id, index) {
let display = new MenuItem(name, id);
this._menuDisplays.push(display);
display.connect('state-changed', Lang.bind(this, function (display) {
let activated = display.getState() != MENU_UNSELECTED;
if (!activated && display == this._activeMenu) {
this._activeMenuIndex = -1;
this._activeMenu = null;
} else if (activated) {
if (display != this._activeMenu && this._activeMenu != null)
this._activeMenu.setState(MENU_UNSELECTED);
this._activeMenuIndex = index;
this._activeMenu = display;
if (id == null) {
this._activeMenuApps = this._getMostUsed();
} else {
this._activeMenuApps = this._appSystem.get_applications_for_menu(id);
}
}
this._redisplay(GenericDisplay.RedisplayFlags.FULL);
}));
this._menuDisplay.append(display.actor, 0);
},
_redisplayMenus: function() {
this._menuDisplay.remove_all();
this._addMenuItem(_("Frequent"), null, 'gtk-select-all');
// Adding an empty box here results in double spacing between
// "Frequent" and the other items.
let separator_actor = new Big.Box();
this._menuDisplay.append(separator_actor, 0);
for (let i = 0; i < this._menus.length; i++) {
let menu = this._menus[i];
this._addMenuItem(menu.name, menu.id, i+1);
}
},
_addApp: function(appInfo) {
let appId = appInfo.get_id();
this._allItems[appId] = appInfo;
@ -363,33 +217,33 @@ AppDisplay.prototype = {
}
} else {
// Loop over the toplevel menu items, load the set of desktop file ids
// associated with each one, skipping empty menus
// associated with each one.
let allMenus = this._appSystem.get_menus();
this._menus = [];
for (let i = 0; i < allMenus.length; i++) {
let menu = allMenus[i];
let menuApps = this._appSystem.get_applications_for_menu(menu.id);
let hasVisibleApps = menuApps.some(function (app) { return !app.get_is_nodisplay(); });
if (!hasVisibleApps) {
continue;
}
this._menus.push(menu);
for (let j = 0; j < menuApps.length; j++) {
let app = menuApps[j];
this._addApp(app);
}
}
this._redisplayMenus();
}
this._appsStale = false;
return false;
},
// Stub this out; the app display always has a category selected
_setDefaultList : function() {
this._matchedItems = {};
this._matchedItems = this._allItems;
this._matchedItemKeys = [];
for (let itemId in this._matchedItems) {
let app = this._allItems[itemId];
if (app.get_is_nodisplay())
continue;
this._matchedItemKeys.push(itemId);
}
this._matchedItemKeys.sort(Lang.bind(this, this._compareItems));
},
// Compares items associated with the item ids based on the alphabetical order
@ -409,25 +263,7 @@ AppDisplay.prototype = {
// Don't show nodisplay items here
if (itemInfo.get_is_nodisplay())
return false;
// Search takes precedence; not typically useful to search within a
// menu
if (this._activeMenu == null || search != "")
return this._isInfoMatchingSearch(itemInfo, search);
else
return this._isInfoMatchingMenu(itemInfo);
},
_isInfoMatchingMenu: function(itemInfo) {
let id = itemInfo.get_id();
for (let i = 0; i < this._activeMenuApps.length; i++) {
let activeApp = this._activeMenuApps[i];
if (activeApp.get_id() == id)
return true;
}
return false;
},
_isInfoMatchingSearch: function(itemInfo, search) {
if (search == null || search == '')
return true;
@ -455,13 +291,6 @@ AppDisplay.prototype = {
return true;
}
if (this._menuSearchAppMatches[search]) {
if (this._menuSearchAppMatches[search].hasOwnProperty(itemInfo.get_id()))
return true;
} else {
log("Missing an entry for search term " + search + " in this._menuSearchAppMatches");
}
return false;
},

View File

@ -75,15 +75,15 @@ function _getIndexWrapped(index, increment, length) {
return (index + increment + length) % length;
}
function _createDisplay(displayType) {
function _createDisplay(displayType, flags) {
if (displayType == APPS)
return new AppDisplay.AppDisplay();
return new AppDisplay.AppDisplay(false, flags);
else if (displayType == PREFS)
return new AppDisplay.AppDisplay(true);
return new AppDisplay.AppDisplay(true, flags);
else if (displayType == DOCS)
return new DocDisplay.DocDisplay();
return new DocDisplay.DocDisplay(flags);
else if (displayType == PLACES)
return new PlaceDisplay.PlaceDisplay();
return new PlaceDisplay.PlaceDisplay(flags);
return null;
}
@ -162,32 +162,20 @@ Pane.prototype = {
}
Signals.addSignalMethods(Pane.prototype);
function ResultArea(displayType, enableNavigation) {
this._init(displayType, enableNavigation);
function ResultArea(displayType, flags) {
this._init(displayType, flags);
}
ResultArea.prototype = {
_init : function(displayType, enableNavigation) {
_init : function(displayType, flags) {
this.actor = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL });
this.resultsContainer = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
spacing: DEFAULT_PADDING
});
this.actor.append(this.resultsContainer, Big.BoxPackFlags.EXPAND);
this.navContainer = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL });
this.resultsContainer.append(this.navContainer, Big.BoxPackFlags.NONE);
this.display = _createDisplay(displayType);
this.navArea = this.display.getNavigationArea();
if (enableNavigation && this.navArea)
this.navContainer.append(this.navArea, Big.BoxPackFlags.EXPAND);
this.display = _createDisplay(displayType, flags);
this.resultsContainer.append(this.display.actor, Big.BoxPackFlags.EXPAND);
this.controlBox = new Big.Box({ x_align: Big.BoxAlignment.CENTER });
this.controlBox.append(this.display.displayControl, Big.BoxPackFlags.NONE);
this.actor.append(this.controlBox, Big.BoxPackFlags.NONE);
this.display.load();
}
}
@ -235,8 +223,8 @@ ResultPane.prototype = {
// Create a display of displayType and pack it into this pane's
// content area. Return the display.
packResults: function(displayType, enableNavigation) {
let resultArea = new ResultArea(displayType, enableNavigation);
packResults: function(displayType) {
let resultArea = new ResultArea(displayType);
createPaneForDetails(this._dash, resultArea.display);
@ -713,7 +701,7 @@ Dash.prototype = {
this._appsSection.header.moreLink.connect('activated', Lang.bind(this, function (link) {
if (this._moreAppsPane == null) {
this._moreAppsPane = new ResultPane(this);
this._moreAppsPane.packResults(APPS, true);
this._moreAppsPane.packResults(APPS);
this._addPane(this._moreAppsPane);
link.setPane(this._moreAppsPane);
}
@ -741,7 +729,7 @@ Dash.prototype = {
this._docsSection.header.moreLink.connect('activated', Lang.bind(this, function (link) {
if (this._moreDocsPane == null) {
this._moreDocsPane = new ResultPane(this);
this._moreDocsPane.packResults(DOCS, true);
this._moreDocsPane.packResults(DOCS);
this._addPane(this._moreDocsPane);
link.setPane(this._moreDocsPane);
}
@ -796,8 +784,7 @@ Dash.prototype = {
this._showSingleSearchSection(section.type);
}));
this._searchResultsSection.content.append(section.header.actor, Big.BoxPackFlags.NONE);
section.resultArea = new ResultArea(section.type, false);
section.resultArea.controlBox.hide();
section.resultArea = new ResultArea(section.type, GenericDisplay.GenericDisplayFlags.DISABLE_VSCROLLING);
this._searchResultsSection.content.append(section.resultArea.actor, Big.BoxPackFlags.EXPAND);
createPaneForDetails(this, section.resultArea.display);
}
@ -847,6 +834,11 @@ Dash.prototype = {
}
}
// Here work around a bug that I never quite tracked down
// the root cause of; it appeared that the search results
// section was getting a 0 height allocation.
this._searchResultsSection.content.queue_relayout();
return false;
},
@ -914,7 +906,6 @@ Dash.prototype = {
if (section.type == type) {
// This will be the only section shown.
section.resultArea.display.selectFirstItem();
section.resultArea.controlBox.show();
let itemCount = section.resultArea.display.getMatchedItemsCount();
let itemCountText = itemCount + "";
section.header.actor.hide();
@ -938,8 +929,6 @@ Dash.prototype = {
let section = this._searchSections[i];
if (section.type == this._searchResultsSingleShownSection) {
// This will no longer be the only section shown.
section.resultArea.display.displayPage(0);
section.resultArea.controlBox.hide();
let itemCount = section.resultArea.display.getMatchedItemsCount();
if (itemCount != 0) {
section.header.actor.show();

View File

@ -111,19 +111,19 @@ DocDisplayItem.prototype = {
/* This class represents a display containing a collection of document items.
* The documents are sorted by how recently they were last visited.
*/
function DocDisplay() {
this._init();
function DocDisplay(flags) {
this._init(flags);
}
DocDisplay.prototype = {
__proto__: GenericDisplay.GenericDisplay.prototype,
_init : function() {
GenericDisplay.GenericDisplay.prototype._init.call(this);
_init : function(flags) {
GenericDisplay.GenericDisplay.prototype._init.call(this, flags);
// We keep a single timeout callback for updating last visited times
// for all the items in the display. This avoids creating individual
// callbacks for each item in the display. So proper time updates
// for individual items and item details depend on the item being
// for individual items and item details depend on the item being
// associated with one of the displays.
this._updateTimeoutTargetTime = -1;
this._updateTimeoutId = 0;

View File

@ -11,6 +11,7 @@ const Meta = imports.gi.Meta;
const Pango = imports.gi.Pango;
const Signals = imports.signals;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Button = imports.ui.button;
const DND = imports.ui.dnd;
@ -18,7 +19,6 @@ const Link = imports.ui.link;
const Main = imports.ui.main;
const RedisplayFlags = { NONE: 0,
RESET_CONTROLS: 1 << 0,
FULL: 1 << 1,
SUBSEARCH: 1 << 2,
IMMEDIATE: 1 << 3 };
@ -320,31 +320,36 @@ GenericDisplayItem.prototype = {
Signals.addSignalMethods(GenericDisplayItem.prototype);
const GenericDisplayFlags = {
DISABLE_VSCROLLING: 1 << 0
}
/* This is a virtual class that represents a display containing a collection of items
* that can be filtered with a search string.
*/
function GenericDisplay() {
this._init();
function GenericDisplay(flags) {
this._init(flags);
}
GenericDisplay.prototype = {
_init : function() {
_init : function(flags) {
let disableVScrolling = (flags & GenericDisplayFlags.DISABLE_VSCROLLING) != 0;
this._search = '';
this._expanded = false;
this._maxItemsPerPage = null;
this._list = new Shell.OverflowList({ spacing: 6.0,
item_height: ITEM_DISPLAY_HEIGHT });
this._list.connect('notify::n-pages', Lang.bind(this, function () {
this._updateDisplayControl(true);
}));
this._list.connect('notify::page', Lang.bind(this, function () {
this._updateDisplayControl(false);
}));
if (disableVScrolling) {
this.actor = this._list = new Shell.OverflowList({ spacing: 6,
item_height: ITEM_DISPLAY_HEIGHT });
} else {
this.actor = new St.ScrollView({ x_fill: true, y_fill: true });
this.actor.get_hscroll_bar().hide();
this._list = new St.BoxLayout({ style_class: "generic-display-container",
vertical: true });
this.actor.add_actor(this._list);
}
this._pendingRedisplay = RedisplayFlags.NONE;
this._list.connect('notify::mapped', Lang.bind(this, this._onMappedNotify));
this.actor.connect('notify::mapped', Lang.bind(this, this._onMappedNotify));
// map<itemId, Object> where Object represents the item info
this._allItems = {};
@ -356,13 +361,6 @@ GenericDisplay.prototype = {
this._displayedItems = {};
this._openDetailIndex = -1;
this._selectedIndex = -1;
// These two are public - .actor is the normal "actor subclass" property,
// but we also expose a .displayControl actor which is separate.
// See also getNavigationArea.
this.actor = this._list;
this.displayControl = new Big.Box({ background_color: ITEM_DISPLAY_BACKGROUND_COLOR,
spacing: 12,
orientation: Big.BoxOrientation.HORIZONTAL});
},
//// Public methods ////
@ -373,7 +371,7 @@ GenericDisplay.prototype = {
if (lowertext == this._search) {
return;
}
let flags = RedisplayFlags.RESET_CONTROLS | RedisplayFlags.IMMEDIATE;
let flags = RedisplayFlags.IMMEDIATE;
if (this._search != '') {
// Because we combine search terms with OR, we have to be sure that no new term
// was introduced before deciding that the new search results will be a subset of
@ -401,7 +399,7 @@ GenericDisplay.prototype = {
// to the bottom one. Returns true if the selection actually moved up, false if it wrapped
// around to the bottom.
selectUp: function() {
let count = this._list.displayedCount;
let count = this._getVisibleCount();
let selectedUp = true;
let prev = this._selectedIndex - 1;
if (this._selectedIndex <= 0) {
@ -416,7 +414,7 @@ GenericDisplay.prototype = {
// to the top one. Returns true if the selection actually moved down, false if it wrapped
// around to the top.
selectDown: function() {
let count = this._list.displayedCount;
let count = this._getVisibleCount();
let selectedDown = true;
let next = this._selectedIndex + 1;
if (this._selectedIndex == count - 1) {
@ -435,7 +433,7 @@ GenericDisplay.prototype = {
// Selects the last item among the displayed items.
selectLastItem: function() {
let count = this._list.displayedCount;
let count = this._getVisibleCount();
if (this.hasItems())
this._selectIndex(count - 1);
},
@ -472,6 +470,8 @@ GenericDisplay.prototype = {
resetState: function() {
this._filterReset();
this._openDetailIndex = -1;
if (!(this.actor instanceof Shell.OverflowList))
this.actor.get_vscroll_bar().get_adjustment().value = 0;
},
// Returns an actor which acts as a sidebar; this is used for
@ -485,15 +485,6 @@ GenericDisplay.prototype = {
return item.createDetailsActor();
},
// Displays the page specified by the pageNumber argument.
displayPage: function(pageNumber) {
// Cleanup from the previous selection, but don't unset this._selectedIndex
if (this.hasSelected()) {
this._findDisplayedByIndex(this._selectedIndex).markSelected(false);
}
this._list.page = pageNumber;
},
//// Protected methods ////
_recreateDisplayItems: function() {
@ -519,14 +510,14 @@ GenericDisplay.prototype = {
Lang.bind(this,
function() {
// update the selection
this._selectIndex(this._list.get_actor_index(displayItem.actor));
this._selectIndex(this._list.get_children().indexOf(displayItem.actor));
this.activateSelected();
}));
displayItem.connect('show-details',
Lang.bind(this,
function() {
let index = this._list.get_actor_index(displayItem.actor);
let index = this._list.get_children().indexOf(displayItem.actor);
/* Close the details pane if already open */
if (index == this._openDetailIndex) {
this._openDetailIndex = -1;
@ -541,9 +532,10 @@ GenericDisplay.prototype = {
// Removes an item identifed by the itemId from the displayed items.
_removeDisplayItem: function(itemId) {
let count = this._list.displayedCount;
let children = this._list.get_children();
let count = children.length;
let displayItem = this._displayedItems[itemId];
let displayItemIndex = this._list.get_actor_index(displayItem.actor);
let displayItemIndex = children.indexOf(displayItem.actor);
if (this.hasSelected() && count == 1) {
this.unsetSelected();
@ -638,28 +630,22 @@ GenericDisplay.prototype = {
/*
* Updates the displayed items, applying the search string if one exists.
* @flags: Flags controlling redisplay behavior as follows:
* RESET_CONTROLS - indicates if the page selection should be reset when displaying the matching results.
* We reset the page selection when the change in results was initiated by the user by
* entering a different search criteria or by viewing the results list in a different
* size mode, but we keep the page selection the same if the results got updated on
* their own while the user was browsing through the result pages.
* SUBSEARCH - Indicates that the current _search is a superstring of the previous
* one, which implies we only need to re-search through previous results.
* FULL - Indicates that we need recreate all displayed items; implies RESET_CONTROLS as well
* FULL - Indicates that we need recreate all displayed items.
* IMMEDIATE - Do the full redisplay even if we're not mapped. This is useful
* if you want to get the number of matched items and show/hide a section based on
* that number.
*/
_redisplay: function(flags) {
let immediate = (flags & RedisplayFlags.IMMEDIATE) > 0;
if (!immediate && !this._list.mapped) {
let immediate = (flags & RedisplayFlags.IMMEDIATE) != 0;
if (!immediate && !this.actor.mapped) {
this._pendingRedisplay |= flags;
return;
}
let isSubSearch = (flags & RedisplayFlags.SUBSEARCH) > 0;
let fullReload = (flags & RedisplayFlags.FULL) > 0;
let resetPage = (flags & RedisplayFlags.RESET_CONTROLS) > 0 || fullReload;
let isSubSearch = (flags & RedisplayFlags.SUBSEARCH) != 0;
let fullReload = (flags & RedisplayFlags.FULL) != 0;
let hadSelected = this.hasSelected();
this.unsetSelected();
@ -681,9 +667,6 @@ GenericDisplay.prototype = {
this._redisplayReordering();
}
if (resetPage)
this._list.page = 0;
if (hadSelected) {
this._selectedIndex = -1;
this.selectFirstItem();
@ -783,59 +766,10 @@ GenericDisplay.prototype = {
return matchScores;
},
/*
* Updates the display control to reflect the matched items set and the page selected.
*
* resetDisplayControl - indicates if the display control should be re-created because
* the results or the space allocated for them changed. If it's false,
* the existing display control is used and only the page links are
* updated to reflect the current page selection.
*/
_updateDisplayControl: function(resetDisplayControl) {
if (resetDisplayControl) {
this.displayControl.remove_all();
let nPages = this._list.n_pages;
// Don't show the page indicator if there is only one page.
if (nPages == 1)
return;
let pageNumber = this._list.page;
for (let i = 0; i < nPages; i++) {
let pageControl = new Link.Link({ color: (i == pageNumber) ? DISPLAY_CONTROL_SELECTED_COLOR : ITEM_DISPLAY_DESCRIPTION_COLOR,
font_name: "Sans Bold 16px",
text: (i+1) + "",
reactive: (i == pageNumber) ? false : true});
this.displayControl.append(pageControl.actor, Big.BoxPackFlags.NONE);
// we use pageNumberLocalScope to get the page number right in the callback function
let pageNumberLocalScope = i;
pageControl.connect('clicked',
Lang.bind(this,
function(o, event) {
this.displayPage(pageNumberLocalScope);
}));
}
} else {
let pageControlActors = this.displayControl.get_children();
for (let i = 0; i < pageControlActors.length; i++) {
let pageControlActor = pageControlActors[i];
if (i == this._list.page) {
pageControlActor.color = DISPLAY_CONTROL_SELECTED_COLOR;
pageControlActor.reactive = false;
} else {
pageControlActor.color = ITEM_DISPLAY_DESCRIPTION_COLOR;
pageControlActor.reactive = true;
}
}
}
if (this.hasSelected()) {
this.selectFirstItem();
}
},
// Returns a display item based on its index in the ordering of the
// display children.
_findDisplayedByIndex: function(index) {
let actor = this._list.get_displayed_actor(index);
let actor = this._list.get_children()[index];
return this._findDisplayedByActor(actor);
},
@ -869,8 +803,14 @@ GenericDisplay.prototype = {
this.emit('selected');
},
_getVisibleCount: function() {
if (this.actor instanceof Shell.OverflowList)
return this._list.displayed_count;
return this._list.get_n_children();
},
_onMappedNotify: function () {
let mapped = this._list.mapped;
let mapped = this.actor.mapped;
if (mapped && this._pendingRedisplay > RedisplayFlags.NONE)
this._redisplay(this._pendingRedisplay);

View File

@ -470,15 +470,15 @@ PlaceDisplayItem.prototype = {
};
function PlaceDisplay() {
this._init();
function PlaceDisplay(flags) {
this._init(flags);
}
PlaceDisplay.prototype = {
__proto__: GenericDisplay.GenericDisplay.prototype,
_init: function() {
GenericDisplay.GenericDisplay.prototype._init.call(this);
_init: function(flags) {
GenericDisplay.GenericDisplay.prototype._init.call(this, flags);
this._stale = true;
Main.placesManager.connect('places-updated', Lang.bind(this, function (e) {
this._stale = true;