/*
* Sencha GXT 2.3.1a - Sencha for GWT
* Copyright(c) 2007-2013, Sencha, Inc.
* licensing@sencha.com
*
* http://www.sencha.com/products/gxt/license/
*/
package com.extjs.gxt.ui.client.widget.menu;
import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.Style;
import com.extjs.gxt.ui.client.Style.HideMode;
import com.extjs.gxt.ui.client.aria.FocusFrame;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.XDOM;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.ClickRepeaterEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.ContainerEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.MenuEvent;
import com.extjs.gxt.ui.client.event.PreviewEvent;
import com.extjs.gxt.ui.client.util.BaseEventPreview;
import com.extjs.gxt.ui.client.util.ClickRepeater;
import com.extjs.gxt.ui.client.util.KeyNav;
import com.extjs.gxt.ui.client.util.Point;
import com.extjs.gxt.ui.client.util.Size;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.Container;
import com.extjs.gxt.ui.client.widget.Layout;
import com.extjs.gxt.ui.client.widget.layout.MenuLayout;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Accessibility;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
/**
* A menu component.
*
* <dl>
* <dt><b>Events:</b></dt>
*
* <dd><b>BeforeShow</b> : MenuEvent(container)<br>
* <div>Fires before this menu is displayed. Listeners can cancel the action by
* calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>container : this</li>
* </ul>
* </dd>
*
* <dd><b>Show</b> : MenuEvent(container)<br>
* <div>Fires after this menu is displayed.</div>
* <ul>
* <li>container : this</li>
* </ul>
* </dd>
*
* <dd><b>BeforeHide</b> : MenuEvent(container)<br>
* <div>Fired before the menu is hidden. Listeners can cancel the action by
* calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>container : this</li>
* </ul>
* </dd>
*
* <dd><b>Hide</b> : MenuEvent(container)<br>
* <div>Fires after this menu is hidden.</div>
* <ul>
* <li>container : this</li>
* </ul>
* </dd>
*
* <dd><b>BeforeAdd</b> : MenuEvent(container, item, index)<br>
* <div>Fires before a item is added or inserted. Listeners can cancel the
* action by calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>container : this</li>
* <li>item : the item being added</li>
* <li>index : the index at which the item will be added</li>
* </ul>
* </dd>
*
* <dd><b>BeforeRemove</b> : MenuEvent(container, item)<br>
* <div>Fires before a item is removed. Listeners can cancel the action by
* calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>container : this</li>
* <li>item : the item being removed</li>
* </ul>
* </dd>
*
* <dd><b>Add</b> : MenuEvent(container, item, index)<br>
* <div>Fires after a item has been added or inserted.</div>
* <ul>
* <li>container : this</li>
* <li>item : the item that was added</li>
* <li>index : the index at which the item will be added</li>
* </ul>
* </dd>
*
* <dd><b>Remove</b> : MenuEvent(container, item)<br>
* <div>Fires after a item has been removed.</div>
* <ul>
* <li>container : this</li>
* <li>item : the item being removed</li>
* </ul>
* </dd>
* </dl>
*
* <dl>
* <dt>Inherited Events:</dt>
* <dd>BoxComponent Move</dd>
* <dd>BoxComponent Resize</dd>
* <dd>Component Enable</dd>
* <dd>Component Disable</dd>
* <dd>Component BeforeHide</dd>
* <dd>Component Hide</dd>
* <dd>Component BeforeShow</dd>
* <dd>Component Show</dd>
* <dd>Component Attach</dd>
* <dd>Component Detach</dd>
* <dd>Component BeforeRender</dd>
* <dd>Component Render</dd>
* <dd>Component BrowserEvent</dd>
* <dd>Component BeforeStateRestore</dd>
* <dd>Component StateRestore</dd>
* <dd>Component BeforeStateSave</dd>
* <dd>Component SaveState</dd>
* </dl>
*/
public class Menu extends Container<Component> {
protected KeyNav<ComponentEvent> keyNav;
protected Item parentItem;
protected BaseEventPreview eventPreview;
protected boolean plain;
protected boolean showSeparator = true;
protected El ul;
protected Item activeItem;
private String subMenuAlign = "tl-tr?";
private String defaultAlign = "tl-bl?";
private int minWidth = 120;
private boolean showing;
private boolean constrainViewport = true;
private boolean focusOnShow = true;
private int maxHeight = Style.DEFAULT;
private boolean enableScrolling = true;
private int scrollIncrement = 24;
private int scrollerHeight = 8;
private int activeMax;
private Size lastWindowSize;
/**
* Creates a new menu.
*/
public Menu() {
baseStyle = "x-menu";
shim = true;
monitorWindowResize = true;
setShadow(true);
setLayoutOnChange(true);
enableLayout = true;
setLayout(new MenuLayout());
eventPreview = new BaseEventPreview() {
@Override
protected boolean onPreview(PreviewEvent pe) {
Menu.this.onAutoHide(pe);
return super.onPreview(pe);
}
@Override
protected void onPreviewKeyPress(PreviewEvent pe) {
super.onPreviewKeyPress(pe);
onEscape(pe);
}
};
eventPreview.setAutoHide(false);
}
/**
* Adds a item to the menu.
*
* @param item the new item
*/
@Override
public boolean add(Component item) {
return super.add(item);
}
/**
* Returns the default alignment.
*
* @return the default align
*/
public String getDefaultAlign() {
return defaultAlign;
}
@Override
public El getLayoutTarget() {
return ul;
}
/**
* Returns the max height of the menu or -1 if not set.
*
* @return the max height in pixels
*/
public int getMaxHeight() {
return maxHeight;
}
/**
* Returns the menu's minimum width.
*
* @return the width
*/
public int getMinWidth() {
return minWidth;
}
/**
* Returns the menu's parent item.
*
* @return the parent item
*/
public Item getParentItem() {
return parentItem;
}
/**
* Returns the sub menu alignment.
*
* @return the alignment
*/
public String getSubMenuAlign() {
return subMenuAlign;
}
/**
* Hides the menu.
*/
public void hide() {
hide(false);
}
/**
* Hides this menu and optionally all parent menus
*
* @param deep true to close all parent menus
* @return this
*/
public Menu hide(boolean deep) {
if (showing) {
MenuEvent me = new MenuEvent(this);
if (fireEvent(Events.BeforeHide, me)) {
if (activeItem != null) {
activeItem.deactivate();
activeItem = null;
}
onHide();
RootPanel.get().remove(this);
eventPreview.remove();
showing = false;
hidden = true;
fireEvent(Events.Hide, me);
if (deep && parentItem != null) {
parentItem.parentMenu.hide(true);
}
}
}
return this;
}
/**
* Inserts an item into the menu.
*
* @param item the item to insert
* @param index the insert location
*/
@Override
public boolean insert(Component item, int index) {
if (item instanceof Item) {
((Item) item).parentMenu = this;
}
return super.insert(item, index);
}
/**
* Returns true if constrain to viewport is enabled.
*
* @return the constrain to viewport state
*/
public boolean isConstrainViewport() {
return constrainViewport;
}
/**
* Returns true if vertical scrolling is enabled.
*
* @return true for scrolling
*/
public boolean isEnableScrolling() {
return enableScrolling;
}
/**
* Returns true if the menu will be focused when displayed.
*
* @return true if focused
*/
public boolean isFocusOnShow() {
return focusOnShow;
}
@Override
public boolean isVisible() {
return showing;
}
@Override
public void onComponentEvent(ComponentEvent ce) {
super.onComponentEvent(ce);
switch (ce.getEventTypeInt()) {
case Event.ONCLICK:
onClick(ce);
break;
case Event.ONMOUSEMOVE:
onMouseMove(ce);
break;
case Event.ONMOUSEOUT:
onMouseOut(ce);
break;
case Event.ONMOUSEOVER:
onMouseOver(ce);
break;
case Event.ONMOUSEWHEEL:
if (enableScrolling) {
scrollMenu(ce.getEvent().getMouseWheelVelocityY() < 0);
}
}
El t = ce.getTargetEl();
if (enableScrolling && t.is(".x-menu-scroller")) {
switch (ce.getEventTypeInt()) {
case Event.ONMOUSEOVER:
// deactiveActiveItem();
onScrollerIn(t);
break;
case Event.ONMOUSEOUT:
onScrollerOut(t);
break;
}
}
}
/**
* Removes a item from the menu.
*
* @param item the menu to remove
*/
@Override
public boolean remove(Component item) {
if (activeItem == item) {
deactiveActiveItem();
}
boolean success = super.remove(item);
if (success && item instanceof Item) {
((Item) item).parentMenu = null;
}
return success;
}
/**
* Sets the active item. The component must be of type <code>Item</code> to be
* activated. All other types are ignored.
*
* @param c the component to set active
* @param autoExpand true to auto expand the item
*/
public void setActiveItem(Component c, boolean autoExpand) {
if (c == null) {
deactiveActiveItem();
return;
}
if (c instanceof Item) {
Item item = (Item) c;
if (item != activeItem) {
deactiveActiveItem();
this.activeItem = item;
item.activate(autoExpand);
item.el().scrollIntoView(ul.dom, false);
focus();
if (GXT.isFocusManagerEnabled()) {
FocusFrame.get().frame(item);
Accessibility.setState(getElement(), "aria-activedescendant", item.getId());
}
} else if (autoExpand) {
item.expandMenu(autoExpand);
}
}
}
/**
* Sets whether the menu should be constrained to the viewport when shown.
* Only applies when using {@link #showAt(int, int)}.
*
* @param constrainViewport true to constrain
*/
public void setConstrainViewport(boolean constrainViewport) {
this.constrainViewport = constrainViewport;
}
/**
* Sets the default {@link El#alignTo} anchor position value for this menu
* relative to its element of origin (defaults to "tl-bl?").
*
* @param defaultAlign the default align
*/
public void setDefaultAlign(String defaultAlign) {
this.defaultAlign = defaultAlign;
}
/**
* True to enable vertical scrolling of the children in the menu (defaults to
* true).
*
* @param enableScrolling true to for scrolling
*/
public void setEnableScrolling(boolean enableScrolling) {
this.enableScrolling = enableScrolling;
}
/**
* True to set the focus on the menu when it is displayed.
*
* @param focusOnShow true to focus
*/
public void setFocusOnShow(boolean focusOnShow) {
this.focusOnShow = focusOnShow;
}
/**
* Sets the max height of the menu (defaults to -1). Only applies when
* {@link #setEnableScrolling(boolean)} is set to true.
*
* @param maxHeight the max height
*/
public void setMaxHeight(int maxHeight) {
this.maxHeight = maxHeight;
}
/**
* Sets he minimum width of the menu in pixels (defaults to 120).
*
* @param minWidth the min width
*/
public void setMinWidth(int minWidth) {
this.minWidth = minWidth;
}
/**
* The {@link El#alignTo} anchor position value to use for submenus of this
* menu (defaults to "tl-tr-?").
*
* @param subMenuAlign the sub alignment
*/
public void setSubMenuAlign(String subMenuAlign) {
this.subMenuAlign = subMenuAlign;
}
/**
* Displays this menu relative to another element.
*
* @param elem the element to align to
* @param pos the {@link El#alignTo} anchor position to use in aligning to the
* element (defaults to defaultAlign)
*/
public void show(Element elem, String pos) {
show(elem, pos, new int[] {0, 0});
}
/**
* Displays this menu relative to another element.
*
* @param elem the element to align to
* @param pos the {@link El#alignTo} anchor position to use in aligning to the
* element (defaults to defaultAlign)
* @param offsets the menu align offsets
*/
public void show(Element elem, String pos, int[] offsets) {
MenuEvent me = new MenuEvent(this);
if (fireEvent(Events.BeforeShow, me)) {
lastWindowSize = new Size(Window.getClientWidth(), Window.getClientHeight());
RootPanel.get().add(this);
el().makePositionable(true);
onShow();
el().updateZIndex(0);
showing = true;
doAutoSize();
el().alignTo(elem, pos, offsets);
if (enableScrolling) {
constrainScroll(el().getY());
}
el().show();
eventPreview.add();
if (focusOnShow) {
focus();
}
fireEvent(Events.Show, me);
}
}
/**
* Displays this menu relative to the widget using the default alignment.
*
* @param widget the align widget
*/
public void show(Widget widget) {
show(widget.getElement(), defaultAlign);
}
/**
* Displays this menu at a specific xy position.
*
* @param x the x coordinate
* @param y the y coordinate
*/
public void showAt(int x, int y) {
MenuEvent me = new MenuEvent(this);
if (fireEvent(Events.BeforeShow, me)) {
lastWindowSize = new Size(Window.getClientWidth(), Window.getClientHeight());
RootPanel.get().add(this);
el().makePositionable(true);
onShow();
el().updateZIndex(0);
showing = true;
doAutoSize();
if (constrainViewport) {
Point p = el().adjustForConstraints(new Point(x, y));
x = p.x;
y = p.y;
}
setPagePosition(x + XDOM.getBodyScrollLeft(), y + XDOM.getBodyScrollTop());
if (enableScrolling) {
constrainScroll(y);
}
el().show();
eventPreview.add();
if (focusOnShow) {
focus();
}
fireEvent(Events.Show, me);
}
}
@Override
protected void afterRender() {
super.afterRender();
keyNav = new KeyNav<ComponentEvent>(this) {
public void onDown(ComponentEvent ce) {
onKeyDown(ce);
}
public void onEnter(ComponentEvent be) {
if (activeItem != null) {
be.cancelBubble();
activeItem.onClick(be);
}
}
public void onLeft(ComponentEvent be) {
hide();
if (parentItem != null) {
parentItem.parentMenu.focus();
if (GXT.isFocusManagerEnabled()) {
FocusFrame.get().frame(parentItem);
}
} else {
Menu menu = Menu.this;
while (menu.parentItem != null) {
menu = menu.parentItem.parentMenu;
}
menu.fireEvent(Events.Minimize);
}
}
public void onRight(ComponentEvent be) {
if (activeItem != null) {
activeItem.expandMenu(true);
}
if (activeItem instanceof MenuItem) {
MenuItem mi = (MenuItem) activeItem;
if (mi.subMenu != null && mi.subMenu.isVisible()) {
return;
}
}
Menu menu = Menu.this;
while (menu.parentItem != null) {
menu = menu.parentItem.parentMenu;
}
menu.fireEvent(Events.Maximize);
}
public void onUp(ComponentEvent ce) {
onKeyUp(ce);
}
};
}
protected void constrainScroll(int y) {
int full = ul.setHeight("auto").getHeight();
int max = maxHeight != Style.DEFAULT ? maxHeight : (XDOM.getViewHeight(false) - y);
if (full > max && max > 0) {
activeMax = max - 10 - scrollerHeight * 2;
ul.setHeight(activeMax, true);
createScrollers();
} else {
ul.setHeight(full, true);
NodeList<Element> nodes = el().select(".x-menu-scroller");
for (int i = 0; i < nodes.getLength(); i++) {
fly(nodes.getItem(i)).hide();
}
}
ul.setScrollTop(0);
}
@Override
protected ComponentEvent createComponentEvent(Event event) {
return new MenuEvent(this);
}
@Override
protected ContainerEvent<Menu, Component> createContainerEvent(Component item) {
return new MenuEvent(this, item);
}
protected void createScrollers() {
if (el().select(".x-menu-scroller").getLength() == 0) {
Listener<ClickRepeaterEvent> listener = new Listener<ClickRepeaterEvent>() {
public void handleEvent(ClickRepeaterEvent be) {
onScroll(be);
}
};
El scroller;
scroller = new El(DOM.createDiv());
scroller.addStyleName("x-menu-scroller", "x-menu-scroller-top");
scroller.setInnerHtml(" ");
ClickRepeater cr = new ClickRepeater(scroller);
cr.doAttach();
cr.addListener(Events.OnClick, listener);
addAttachable(cr);
el().insertFirst(scroller.dom);
scroller = new El(DOM.createDiv());
scroller.addStyleName("x-menu-scroller", "x-menu-scroller-bottom");
scroller.setInnerHtml(" ");
cr = new ClickRepeater(scroller);
cr.doAttach();
cr.addListener(Events.OnClick, listener);
addAttachable(cr);
el().appendChild(scroller.dom);
}
}
protected void deactiveActiveItem() {
if (activeItem != null) {
activeItem.deactivate();
activeItem = null;
}
if (GXT.isFocusManagerEnabled()) {
FocusFrame.get().unframe();
Accessibility.setState(getElement(), "aria-activedescendant", "");
}
}
protected void doAutoSize() {
if (showing && width == null) {
int width = getLayoutTarget().getWidth() + el().getFrameWidth("lr");
el().setWidth(Math.max(width, minWidth), true);
}
}
protected boolean onAutoHide(PreviewEvent pe) {
if ((pe.getEventTypeInt() == Event.ONMOUSEDOWN || pe.getEventTypeInt() == Event.ONMOUSEWHEEL
|| pe.getEventTypeInt() == Event.ONSCROLL || pe.getEventTypeInt() == Event.ONKEYPRESS)
&& !(pe.within(getElement()) || (fly(pe.getTarget()).findParent(".x-ignore", -1) != null))) {
MenuEvent me = new MenuEvent(this);
me.setEvent(pe.getEvent());
if (fireEvent(Events.AutoHide, me)) {
hide(true);
return true;
}
}
return false;
}
protected void onClick(ComponentEvent ce) {
Component item = findItem(ce.getTarget());
if (item != null && item instanceof Item) {
((Item) item).onClick(ce);
}
}
@Override
protected void onDetach() {
super.onDetach();
if (eventPreview != null) {
eventPreview.remove();
}
}
protected void onEscape(PreviewEvent pe) {
if (pe.getKeyCode() == KeyCodes.KEY_ESCAPE) {
if (activeItem != null && !activeItem.onEscape()) {
return;
}
hide(false);
}
}
@Override
protected void onHide() {
super.onHide();
deactiveActiveItem();
}
@Override
protected void onInsert(Component item, int index) {
super.onInsert(item, index);
if (rendered && GXT.isAriaEnabled() && item instanceof CheckMenuItem) {
handleRadioGroups();
}
}
protected void onKeyDown(ComponentEvent ce) {
ce.stopEvent();
if (tryActivate(indexOf(activeItem) + 1, 1) == null) {
tryActivate(0, 1);
}
}
protected void onKeyUp(ComponentEvent ce) {
ce.stopEvent();
if (tryActivate(indexOf(activeItem) - 1, -1) == null) {
tryActivate(getItemCount() - 1, -1);
}
}
@Override
protected void onLayoutExcecuted(Layout layout) {
super.onLayoutExcecuted(layout);
doAutoSize();
}
protected void onMouseMove(ComponentEvent ce) {
Component c = findItem(ce.getTarget());
if (c != null && c instanceof Item) {
Item item = (Item) c;
if (activeItem != item && item.canActivate && item.isEnabled()) {
setActiveItem(item, true);
}
}
}
protected void onMouseOut(ComponentEvent ce) {
EventTarget to = ce.getEvent().getRelatedEventTarget();
if (activeItem != null
&& (to == null || (Element.is(to) && !DOM.isOrHasChild(activeItem.getElement(), (Element) Element.as(to))))
&& activeItem.shouldDeactivate(ce)) {
deactiveActiveItem();
}
}
protected void onMouseOver(ComponentEvent ce) {
EventTarget from = ce.getEvent().getRelatedEventTarget();
if (from == null || (Element.is(from) && !DOM.isOrHasChild(getElement(), (Element) Element.as(from)))) {
Component c = findItem(ce.getTarget());
if (c != null && c instanceof Item) {
Item item = (Item) c;
if (activeItem != item && item.canActivate && item.isEnabled()) {
setActiveItem(item, true);
}
}
}
}
@Override
protected void onRemove(Component item) {
super.onRemove(item);
if (rendered && GXT.isAriaEnabled() && item instanceof CheckMenuItem) {
handleRadioGroups();
}
}
@Override
protected void onRender(Element target, int index) {
setElement(DOM.createDiv(), target, index);
el().makePositionable(true);
super.onRender(target, index);
ul = new El(DOM.createDiv());
ul.addStyleName(baseStyle + "-list");
getElement().appendChild(ul.dom);
// add menu to ignore list
eventPreview.getIgnoreList().add(getElement());
el().setTabIndex(0);
el().setElementAttribute("hideFocus", "true");
el().addStyleName("x-ignore");
if (GXT.isAriaEnabled()) {
Accessibility.setRole(getElement(), "menu");
Accessibility.setRole(ul.dom, "presentation");
handleRadioGroups();
}
if (plain) {
addStyleName("x-menu-plain");
}
if (!showSeparator) {
addStyleName("x-menu-nosep");
}
sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.KEYEVENTS | Event.ONMOUSEWHEEL);
}
protected void onScroll(ClickRepeaterEvent ce) {
El target = ce.getEl();
boolean top = target.is(".x-menu-scroller-top");
scrollMenu(top);
if (top ? ul.getScrollTop() <= 0 : ul.getScrollTop() + activeMax >= ul.dom.getPropertyInt("scrollHeight")) {
onScrollerOut(target);
}
}
protected void onScrollerIn(El t) {
boolean top = t.is(".x-menu-scroller-top");
if (top ? ul.getScrollTop() > 0 : ul.getScrollTop() + activeMax < ul.dom.getPropertyInt("scrollHeight")) {
t.addStyleName("x-menu-item-active", "x-menu-scroller-active");
}
}
protected void onScrollerOut(El t) {
t.removeStyleName("x-menu-item-active", "x-menu-scroller-active");
}
@Override
protected void onWindowResize(int width, int height) {
//EXTGWT-3032 Window resize event firing when menu shown over iframe in ie 8
if (GXT.isIE8 && lastWindowSize != null) {
if (lastWindowSize.width == width && lastWindowSize.height == height) {
return;
}
}
hide(true);
}
protected void scrollMenu(boolean top) {
ul.setScrollTop(ul.getScrollTop() + scrollIncrement * (top ? -1 : 1));
}
protected Item tryActivate(int start, int step) {
for (int i = start, len = getItemCount(); i >= 0 && i < len; i += step) {
Component c = getItem(i);
if (c instanceof Item) {
Item item = (Item) c;
if (item.canActivate && item.isEnabled()) {
setActiveItem(item, false);
return item;
}
}
}
return null;
}
private void clearGroups() {
NodeList<Element> groups = el().select(".x-menu-radio-group");
for (int i = 0; i < groups.getLength(); i++) {
Element e = groups.getItem(i);
El.fly(e).removeFromParent();
}
}
private El getGroup(String groupName) {
El g = el().selectNode("#" + getId() + "-" + groupName);
if (g == null) {
g = new El(DOM.createDiv());
g.makePositionable(true);
g.dom.setAttribute("role", "group");
g.addStyleName(HideMode.OFFSETS.value());
g.addStyleName("x-menu-radio-group");
g.setId(getId() + "-" + groupName);
el().appendChild(g.dom);
}
return g;
}
private void handleRadioGroups() {
clearGroups();
for (int i = 0; i < getItemCount(); i++) {
Object obj = getItem(i);
if (obj instanceof CheckMenuItem) {
CheckMenuItem check = (CheckMenuItem) obj;
if (check.getGroup() != null) {
El g = getGroup(check.getGroup());
Accessibility.setState(g.dom, "aria-owns", g.dom.getAttribute("aria-owns") + " " + check.getId());
if (check.getAriaGroupTitle() != null) {
g.dom.setTitle(check.getAriaGroupTitle());
}
}
}
}
}
}