runelite-api: fix widget children lookup logic

Also update dev tools panel widget tree to use show all widgets children,
and start from the root widgets.

Thanks to @rsbmatt for his assistance in explaining the logic
This commit is contained in:
Adam
2017-08-05 14:11:05 -04:00
parent b26ee3f193
commit 39f9cfbe70
9 changed files with 330 additions and 202 deletions

View File

@@ -26,6 +26,7 @@ package net.runelite.api;
import java.awt.Canvas; import java.awt.Canvas;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo; import net.runelite.api.widgets.WidgetInfo;
import net.runelite.rs.api.ItemComposition; import net.runelite.rs.api.ItemComposition;
@@ -213,13 +214,14 @@ public class Client
return client.getBaseY(); return client.getBaseY();
} }
public Widget[][] getWidgets() public Widget[] getWidgetRoots()
{ {
return Arrays.stream(client.getWidgets()) int topGroup = client.getWidgetRoot();
.map(parent -> parent != null ? Arrays.stream(parent) return Arrays.stream(client.getWidgets()[topGroup])
.map(child -> child != null ? new Widget(this, child) : null) .filter(Objects::nonNull)
.toArray(Widget[]::new) : null .filter(w -> w.getParentId() == -1) // is a root
).toArray(Widget[][]::new); .map(w -> new Widget(this, w))
.toArray(Widget[]::new);
} }
public Widget getWidget(WidgetInfo widget) public Widget getWidget(WidgetInfo widget)
@@ -230,6 +232,21 @@ public class Client
return getWidget(groupId, childId); return getWidget(groupId, childId);
} }
public Widget[] getGroup(int groupId)
{
net.runelite.rs.api.Widget[][] widgets = client.getWidgets();
if (widgets == null || groupId < 0 || groupId >= widgets.length)
{
return null;
}
return Arrays.stream(widgets[groupId])
.filter(Objects::nonNull)
.map(w -> new Widget(this, w))
.toArray(Widget[]::new);
}
public Widget getWidget(int groupId, int childId) public Widget getWidget(int groupId, int childId)
{ {
net.runelite.rs.api.Widget[][] widgets = client.getWidgets(); net.runelite.rs.api.Widget[][] widgets = client.getWidgets();
@@ -258,11 +275,6 @@ public class Client
return client.getWidgetPositionsY(); return client.getWidgetPositionsY();
} }
public boolean[] getValidInterfaces()
{
return client.getValidInterfaces();
}
public String[] getPlayerOptions() public String[] getPlayerOptions()
{ {
return client.getPlayerOptions(); return client.getPlayerOptions();

View File

@@ -26,13 +26,17 @@ package net.runelite.api.widgets;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
import net.runelite.api.Client; import net.runelite.api.Client;
import net.runelite.api.Node; import net.runelite.api.Node;
import net.runelite.api.Point; import net.runelite.api.Point;
import net.runelite.api.WidgetNode; import net.runelite.api.WidgetNode;
import net.runelite.api.XHashTable; import net.runelite.api.XHashTable;
import static net.runelite.api.widgets.WidgetInfo.TO_CHILD;
import static net.runelite.api.widgets.WidgetInfo.TO_GROUP;
public class Widget public class Widget
{ {
@@ -70,7 +74,7 @@ public class Widget
return null; return null;
} }
return client.getWidget(id >>> 16, id & 0xFFFF); return client.getWidget(TO_GROUP(id), TO_CHILD(id));
} }
public int getParentId() public int getParentId()
@@ -81,7 +85,7 @@ public class Widget
return parentId; return parentId;
} }
int i = getId() >>> 16; int i = TO_GROUP(getId());
XHashTable componentTable = client.getComponentTable(); XHashTable componentTable = client.getComponentTable();
for (Node node : componentTable.getNodes()) for (Node node : componentTable.getNodes())
{ {
@@ -96,6 +100,70 @@ public class Widget
return -1; return -1;
} }
public Widget getChild(int index)
{
net.runelite.rs.api.Widget[] widgets = widget.getChildren();
if (widgets == null || widgets[index] == null)
{
return null;
}
return new Widget(client, widgets[index]);
}
public Widget[] getDynamicChildren()
{
net.runelite.rs.api.Widget[] children = widget.getChildren();
if (children == null)
{
return new Widget[0];
}
return Arrays.stream(children)
.filter(Objects::nonNull)
.filter(w -> w.getParentId() == getId())
.map(w -> new Widget(client, w))
.toArray(Widget[]::new);
}
public Widget[] getStaticChildren()
{
return Arrays.stream(client.getGroup(TO_GROUP(getId())))
.filter(Objects::nonNull)
.filter(w -> w.getParentId() == getId())
.toArray(Widget[]::new);
}
public Widget[] getNestedChildren()
{
XHashTable componentTable = client.getComponentTable();
int group = -1;
// XXX can actually use hashtable lookup instead of table
// iteration here...
for (Node node : componentTable.getNodes())
{
WidgetNode wn = (WidgetNode) node;
if (wn.getHash() == getId())
{
group = wn.getId();
break;
}
}
if (group == -1)
{
return new Widget[0];
}
return Arrays.stream(client.getGroup(group))
.filter(w -> w.getParentId() == getId())
.toArray(Widget[]::new);
}
private int getRelativeX() private int getRelativeX()
{ {
return widget.getRelativeX(); return widget.getRelativeX();
@@ -264,37 +332,6 @@ public class Widget
return widget.getPaddingY(); return widget.getPaddingY();
} }
public Widget[] getChildren()
{
net.runelite.rs.api.Widget[] widgets = widget.getChildren();
if (widgets == null)
{
return null;
}
Widget[] children = new Widget[widgets.length];
for (int i = 0; i < widgets.length; ++i)
{
children[i] = getChild(i);
}
return children;
}
public Widget getChild(int index)
{
net.runelite.rs.api.Widget[] widgets = widget.getChildren();
if (widgets == null || widgets[index] == null)
{
return null;
}
return new Widget(client, widgets[index]);
}
public int getItemId() public int getItemId()
{ {
return widget.getItemId(); return widget.getItemId();

View File

@@ -89,4 +89,14 @@ public enum WidgetInfo
return childId; return childId;
} }
public static int TO_GROUP(int id)
{
return id >>> 16;
}
public static int TO_CHILD(int id)
{
return id & 0xFFFF;
}
} }

View File

@@ -28,6 +28,7 @@ import java.awt.Font;
import java.awt.GraphicsEnvironment; import java.awt.GraphicsEnvironment;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import net.runelite.api.widgets.Widget;
import net.runelite.client.RuneLite; import net.runelite.client.RuneLite;
import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.Plugin;
@@ -51,9 +52,8 @@ public class DevTools extends Plugin
private boolean toggleDecor; private boolean toggleDecor;
private boolean toggleInventory; private boolean toggleInventory;
private int widgetParent = -1; Widget currentWidget;
private int widgetChild = -1; int itemIndex = -1;
private int widgetItem = -1;
private Font font; private Font font;
@@ -171,34 +171,4 @@ public class DevTools extends Plugin
return toggleInventory; return toggleInventory;
} }
void setWidgetParent(int id)
{
widgetParent = id;
}
void setWidgetChild(int id)
{
widgetChild = id;
}
void setWidgetItem(int id)
{
widgetItem = id;
}
int getWidgetParent()
{
return widgetParent;
}
int getWidgetChild()
{
return widgetChild;
}
int getWidgetItem()
{
return widgetItem;
}
} }

View File

@@ -324,37 +324,15 @@ public class DevToolsOverlay extends Overlay
public void renderWidgets(Graphics2D graphics) public void renderWidgets(Graphics2D graphics)
{ {
int parentID = plugin.getWidgetParent(); Widget widget = plugin.currentWidget;
int childID = plugin.getWidgetChild(); int itemIndex = plugin.itemIndex;
int itemIndex = plugin.getWidgetItem();
if (parentID == -1) if (widget == null || widget.isHidden())
{ {
return; return;
} }
Widget widgetParent = client.getWidget(parentID, 0); Rectangle childBounds = widget.getBounds();
if (widgetParent == null || widgetParent.isHidden())
{
return;
}
Rectangle parentBounds = widgetParent.getBounds();
graphics.setColor(YELLOW);
graphics.draw(parentBounds);
if (childID == -1)
{
return;
}
Widget widgetChild = client.getWidget(parentID, childID);
if (widgetChild == null || widgetChild.isHidden())
{
return;
}
Rectangle childBounds = widgetChild.getBounds();
graphics.setColor(CYAN); graphics.setColor(CYAN);
graphics.draw(childBounds); graphics.draw(childBounds);
@@ -363,21 +341,21 @@ public class DevToolsOverlay extends Overlay
return; return;
} }
Widget childComponent = widgetChild.getChild(itemIndex); if (widget.getItemId() != ITEM_EMPTY
if (childComponent != null && !childComponent.isHidden() && widget.getItemId() != ITEM_FILLED)
&& childComponent.getItemId() != ITEM_EMPTY
&& childComponent.getItemId() != ITEM_FILLED)
{ {
Rectangle componentBounds = childComponent.getBounds(); Rectangle componentBounds = widget.getBounds();
graphics.setColor(ORANGE); graphics.setColor(ORANGE);
graphics.draw(componentBounds); graphics.draw(componentBounds);
renderWidgetText(graphics, componentBounds, childComponent.getItemId(), YELLOW); renderWidgetText(graphics, componentBounds, widget.getItemId(), YELLOW);
} }
WidgetItem widgetItem = widgetChild.getWidgetItem(itemIndex); WidgetItem widgetItem = widget.getWidgetItem(itemIndex);
if (widgetItem == null) if (widgetItem == null
|| widgetItem.getId() == ITEM_EMPTY
|| widgetItem.getId() == ITEM_FILLED)
{ {
return; return;
} }

View File

@@ -1,5 +1,6 @@
/* /*
* Copyright (c) 2017, Kronos <https://github.com/KronosDesign> * Copyright (c) 2017, Kronos <https://github.com/KronosDesign>
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
@@ -33,14 +34,18 @@ import javax.swing.tree.DefaultTreeModel;
import net.runelite.api.Client; import net.runelite.api.Client;
import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.Widget;
import static net.runelite.api.widgets.WidgetInfo.TO_CHILD;
import static net.runelite.api.widgets.WidgetInfo.TO_GROUP;
import net.runelite.api.widgets.WidgetItem; import net.runelite.api.widgets.WidgetItem;
import net.runelite.client.RuneLite; import net.runelite.client.RuneLite;
import static net.runelite.client.plugins.devtools.DevToolsOverlay.ITEM_EMPTY;
import static net.runelite.client.plugins.devtools.DevToolsOverlay.ITEM_FILLED;
import net.runelite.client.ui.PluginPanel; import net.runelite.client.ui.PluginPanel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DevToolsPanel extends PluginPanel public class DevToolsPanel extends PluginPanel
{ {
private static final Logger logger = LoggerFactory.getLogger(DevToolsPanel.class);
private final EmptyBorder PADDING_BORDER = new EmptyBorder(3, 3, 3, 3); private final EmptyBorder PADDING_BORDER = new EmptyBorder(3, 3, 3, 3);
private final Client client = RuneLite.getClient(); private final Client client = RuneLite.getClient();
@@ -66,8 +71,6 @@ public class DevToolsPanel extends PluginPanel
private DevTools plugin; private DevTools plugin;
private DefaultMutableTreeNode widgetListRoot = new DefaultMutableTreeNode();
private final SettingsTracker settingsTracker = new SettingsTracker(client); private final SettingsTracker settingsTracker = new SettingsTracker(client);
public DevToolsPanel(DevTools plugin) public DevToolsPanel(DevTools plugin)
@@ -170,16 +173,27 @@ public class DevToolsPanel extends PluginPanel
JPanel container = new JPanel(); JPanel container = new JPanel();
container.setLayout(new BorderLayout()); container.setLayout(new BorderLayout());
JTree tree = new JTree(widgetListRoot); JTree tree = new JTree(new DefaultMutableTreeNode());
tree.setRootVisible(false); tree.setRootVisible(false);
tree.setShowsRootHandles(true); tree.setShowsRootHandles(true);
tree.getSelectionModel().addTreeSelectionListener(e -> tree.getSelectionModel().addTreeSelectionListener(e ->
{ {
Object[] path = e.getPath().getPath(); Object selected = tree.getLastSelectedPathComponent();
plugin.setWidgetParent(Integer.parseInt(path[1].toString())); if (selected instanceof WidgetTreeNode)
plugin.setWidgetChild((path.length > 2) ? Integer.parseInt(path[2].toString()) : -1); {
plugin.setWidgetItem((path.length > 3) ? Integer.parseInt(path[3].toString()) : -1); WidgetTreeNode node = (WidgetTreeNode) selected;
setWidgetInfo(); Widget widget = node.getWidget();
plugin.currentWidget = widget;
plugin.itemIndex = widget.getItemId();
setWidgetInfo(widget);
logger.debug("Set widget to {} and item index to {}", widget, widget.getItemId());
}
else if (selected instanceof WidgetItemNode)
{
WidgetItemNode node = (WidgetItemNode) selected;
plugin.itemIndex = node.getWidgetItem().getIndex();
logger.debug("Set item index to {}", plugin.itemIndex);
}
}); });
JScrollPane scrollPane = new JScrollPane(tree); JScrollPane scrollPane = new JScrollPane(tree);
@@ -189,8 +203,8 @@ public class DevToolsPanel extends PluginPanel
JButton refreshWidgetsBtn = new JButton("Refresh Widgets"); JButton refreshWidgetsBtn = new JButton("Refresh Widgets");
refreshWidgetsBtn.addActionListener(e -> refreshWidgetsBtn.addActionListener(e ->
{ {
refreshWidgets(); DefaultMutableTreeNode root = refreshWidgets();
tree.setModel(new DefaultTreeModel(widgetListRoot)); tree.setModel(new DefaultTreeModel(root));
}); });
JPanel btnContainer = new JPanel(); JPanel btnContainer = new JPanel();
@@ -225,18 +239,8 @@ public class DevToolsPanel extends PluginPanel
return container; return container;
} }
private void setWidgetInfo() private void setWidgetInfo(Widget widget)
{ {
int parent = plugin.getWidgetParent();
int child = plugin.getWidgetChild();
if (parent == -1)
{
return;
}
Widget widget = client.getWidget(parent, (child == -1) ? 0 : child);
if (widget == null) if (widget == null)
{ {
return; return;
@@ -247,8 +251,9 @@ public class DevToolsPanel extends PluginPanel
nameLbl.setText("Name: " + widget.getName().trim()); nameLbl.setText("Name: " + widget.getName().trim());
modelLbl.setText("Model ID: " + widget.getModelId()); modelLbl.setText("Model ID: " + widget.getModelId());
textureLbl.setText("Sprite ID: " + widget.getSpriteId()); textureLbl.setText("Sprite ID: " + widget.getSpriteId());
typeLbl.setText("Type: " + widget.getType()); typeLbl.setText("Type: " + widget.getType()
contentTypeLbl.setText("Content Type: " + widget.getContentType()); + " Parent " + (widget.getParentId() == -1 ? -1 : TO_GROUP(widget.getParentId()) + "." + TO_CHILD(widget.getParentId())));
contentTypeLbl.setText("Content Type: " + widget.getContentType() + " Hidden " + widget.isHidden());
} }
private void highlightButton(JButton button) private void highlightButton(JButton button)
@@ -263,83 +268,89 @@ public class DevToolsPanel extends PluginPanel
} }
} }
private void refreshWidgets() private DefaultMutableTreeNode refreshWidgets()
{ {
Widget[][] widgets = client.getWidgets(); Widget[] rootWidgets = client.getWidgetRoots();
boolean[] validInterfaces = client.getValidInterfaces();
DefaultMutableTreeNode root = new DefaultMutableTreeNode(); DefaultMutableTreeNode root = new DefaultMutableTreeNode();
plugin.setWidgetParent(-1); plugin.currentWidget = null;
plugin.setWidgetChild(-1); plugin.itemIndex = -1;
plugin.setWidgetItem(-1);
int idx = -1; for (Widget widget : rootWidgets)
for (Widget[] children : widgets)
{ {
++idx; DefaultMutableTreeNode childNode = addWidget("R", widget);
if (childNode != null)
if (!validInterfaces[idx])
{ {
continue; root.add(childNode);
} }
}
if (children == null) return root;
}
private DefaultMutableTreeNode addWidget(String type, Widget widget)
{
if (widget == null || widget.isHidden())
{
return null;
}
DefaultMutableTreeNode node = new WidgetTreeNode(type, widget);
Widget[] childComponents = widget.getDynamicChildren();
if (childComponents != null)
{
for (Widget component : childComponents)
{ {
continue; DefaultMutableTreeNode childNode = addWidget("D", component);
} if (childNode != null)
DefaultMutableTreeNode parent = new DefaultMutableTreeNode(idx);
root.add(parent);
for (Widget widgetChild : children)
{
if (widgetChild == null || widgetChild.isHidden())
{ {
continue; node.add(childNode);
}
DefaultMutableTreeNode child = new DefaultMutableTreeNode(widgetChild.getId() & 0xFFFF);
parent.add(child);
Widget[] childComponents = widgetChild.getChildren();
if (childComponents != null)
{
int index = -1;
for (Widget component : childComponents)
{
index++;
if (component == null || component.isHidden()
|| component.getItemId() == ITEM_EMPTY
|| component.getItemId() == ITEM_FILLED)
{
continue;
}
child.add(new DefaultMutableTreeNode(index));
}
}
Collection<WidgetItem> items = widgetChild.getWidgetItems();
if (items == null)
{
continue;
}
for (WidgetItem item : items)
{
if (item == null)
{
continue;
}
child.add(new DefaultMutableTreeNode(item.getIndex()));
} }
} }
} }
widgetListRoot = root; childComponents = widget.getStaticChildren();
if (childComponents != null)
{
for (Widget component : childComponents)
{
DefaultMutableTreeNode childNode = addWidget("S", component);
if (childNode != null)
{
node.add(childNode);
}
}
}
childComponents = widget.getNestedChildren();
if (childComponents != null)
{
for (Widget component : childComponents)
{
DefaultMutableTreeNode childNode = addWidget("N", component);
if (childNode != null)
{
node.add(childNode);
}
}
}
Collection<WidgetItem> items = widget.getWidgetItems();
if (items != null)
{
for (WidgetItem item : items)
{
if (item == null)
{
continue;
}
node.add(new WidgetItemNode(item));
}
}
return node;
} }
} }

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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 THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT OWNER OR 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.
*/
package net.runelite.client.plugins.devtools;
import javax.swing.tree.DefaultMutableTreeNode;
import net.runelite.api.widgets.WidgetItem;
class WidgetItemNode extends DefaultMutableTreeNode
{
private final WidgetItem widgetItem;
public WidgetItemNode(WidgetItem widgetItem)
{
super(widgetItem);
this.widgetItem = widgetItem;
}
public WidgetItem getWidgetItem()
{
return widgetItem;
}
@Override
public String toString()
{
return "I " + widgetItem.getIndex();
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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 THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT OWNER OR 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.
*/
package net.runelite.client.plugins.devtools;
import javax.swing.tree.DefaultMutableTreeNode;
import net.runelite.api.widgets.Widget;
import static net.runelite.api.widgets.WidgetInfo.TO_CHILD;
import static net.runelite.api.widgets.WidgetInfo.TO_GROUP;
class WidgetTreeNode extends DefaultMutableTreeNode
{
private final String type;
public WidgetTreeNode(String type, Widget widget)
{
super(widget);
this.type = type;
}
public Widget getWidget()
{
return (Widget) getUserObject();
}
@Override
public String toString()
{
Widget widget = getWidget();
int id = widget.getId();
return type + " " + TO_GROUP(id) + "." + TO_CHILD(id);
}
}

View File

@@ -172,9 +172,6 @@ public interface Client extends GameEngine
@Import("viewportWidth") @Import("viewportWidth")
int getViewportWidth(); int getViewportWidth();
@Import("validInterfaces")
boolean[] getValidInterfaces();
@Import("isResized") @Import("isResized")
boolean isResized(); boolean isResized();
@@ -234,4 +231,12 @@ public interface Client extends GameEngine
@Import(value = "chatCycle", setter = true) @Import(value = "chatCycle", setter = true)
void setChatCycle(int value); void setChatCycle(int value);
/**
* Get the widget top group. widgets[topGroup] contains widgets with
* parentId -1, which are the widget roots.
* @return
*/
@Import("widgetRoot")
int getWidgetRoot();
} }