Java tutorial
/* * Copyright (C) 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.gapid.views; import static com.google.gapid.image.Images.noAlpha; import static com.google.gapid.models.Follower.nullPrefetcher; import static com.google.gapid.models.Thumbnails.THUMB_SIZE; import static com.google.gapid.util.Colors.getRandomColor; import static com.google.gapid.util.Colors.lerp; import static com.google.gapid.util.Loadable.MessageType.Error; import static com.google.gapid.util.Paths.lastCommand; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.gapid.models.Analytics.View; import com.google.gapid.models.ApiContext; import com.google.gapid.models.ApiContext.FilteringContext; import com.google.gapid.models.Capture; import com.google.gapid.models.CommandStream; import com.google.gapid.models.CommandStream.CommandIndex; import com.google.gapid.models.CommandStream.Node; import com.google.gapid.models.Follower; import com.google.gapid.models.Models; import com.google.gapid.models.Thumbnails; import com.google.gapid.proto.service.Service; import com.google.gapid.proto.service.Service.ClientAction; import com.google.gapid.proto.service.api.API; import com.google.gapid.proto.service.path.Path; import com.google.gapid.rpc.Rpc; import com.google.gapid.rpc.RpcException; import com.google.gapid.rpc.SingleInFlight; import com.google.gapid.rpc.UiCallback; import com.google.gapid.server.Client; import com.google.gapid.util.Events; import com.google.gapid.util.Loadable; import com.google.gapid.util.Messages; import com.google.gapid.util.SelectionHandler; import com.google.gapid.views.Formatter.StylingString; import com.google.gapid.widgets.LinkifiedTreeWithImages; import com.google.gapid.widgets.LoadableImage; import com.google.gapid.widgets.LoadableImageWidget; import com.google.gapid.widgets.LoadablePanel; import com.google.gapid.widgets.SearchBox; import com.google.gapid.widgets.Widgets; import org.eclipse.jface.viewers.TreePath; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGBA; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Shell; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.logging.Logger; /** * API command view displaying the commands with their hierarchy grouping in a tree. */ public class CommandTree extends Composite implements Tab, Capture.Listener, CommandStream.Listener, ApiContext.Listener, Thumbnails.Listener { protected static final Logger LOG = Logger.getLogger(CommandTree.class.getName()); private final Client client; private final Models models; private final LoadablePanel<Tree> loading; protected final Tree tree; private final SelectionHandler<Control> selectionHandler; private final SingleInFlight searchController = new SingleInFlight(); public CommandTree(Composite parent, Client client, Models models, Widgets widgets) { super(parent, SWT.NONE); this.client = client; this.models = models; setLayout(new GridLayout(1, false)); SearchBox search = new SearchBox(this, false); loading = LoadablePanel.create(this, widgets, p -> new Tree(p, models, widgets)); tree = loading.getContents(); search.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); loading.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); models.capture.addListener(this); models.commands.addListener(this); models.contexts.addListener(this); models.thumbs.addListener(this); addListener(SWT.Dispose, e -> { models.capture.removeListener(this); models.commands.removeListener(this); models.contexts.removeListener(this); models.thumbs.removeListener(this); }); search.addListener(Events.Search, e -> search(e.text, (e.detail & Events.REGEX) != 0)); selectionHandler = new SelectionHandler<Control>(LOG, tree.getControl()) { @Override protected void updateModel(Event e) { models.analytics.postInteraction(View.Commands, ClientAction.Select); CommandStream.Node node = tree.getSelection(); if (node != null) { CommandIndex index = node.getIndex(); if (index == null) { models.commands.load(node, () -> models.commands.selectCommands(node.getIndex(), false)); } else { models.commands.selectCommands(index, false); } } } }; Menu popup = new Menu(tree.getControl()); Widgets.createMenuItem(popup, "&Edit", SWT.MOD1 + 'E', e -> { CommandStream.Node node = tree.getSelection(); if (node != null && node.getData() != null && node.getCommand() != null) { widgets.editor.showEditPopup(getShell(), lastCommand(node.getData().getCommands()), node.getCommand()); } }); tree.setPopupMenu(popup, node -> node.getData() != null && node.getCommand() != null && CommandEditor.shouldShowEditPopup(node.getCommand())); tree.registerAsCopySource(widgets.copypaste, node -> { models.analytics.postInteraction(View.Commands, ClientAction.Copy); Service.CommandTreeNode data = node.getData(); if (data == null) { // Copy before loaded. Not ideal, but this is unlikely. return new String[] { "Loading..." }; } StringBuilder result = new StringBuilder(); if (data.getGroup().isEmpty() && data.hasCommands()) { result.append(data.getCommands().getTo(0)).append(": "); API.Command cmd = node.getCommand(); if (cmd == null) { // Copy before loaded. Not ideal, but this is unlikely. result.append("Loading..."); } else { result.append(Formatter.toString(cmd, models.constants::getConstants)); } } else { result.append(data.getCommands().getFrom(0)).append(": ").append(data.getGroup()); } return new String[] { result.toString() }; }, true); } private void search(String text, boolean regex) { models.analytics.postInteraction(View.Commands, ClientAction.Search); CommandStream.Node parent = models.commands.getData(); if (parent != null && !text.isEmpty()) { CommandStream.Node selection = tree.getSelection(); if (selection != null) { parent = selection; } searchController.start().listen( Futures.transformAsync(search(searchRequest(parent, text, regex)), r -> getTreePath(models.commands.getData(), Lists.newArrayList(), r.getCommandTreeNode().getIndicesList().iterator())), new UiCallback<TreePath, TreePath>(tree, LOG) { @Override protected TreePath onRpcThread(Rpc.Result<TreePath> result) throws RpcException, ExecutionException { return result.get(); } @Override protected void onUiThread(TreePath result) { select(result); } }); } } private static Service.FindRequest searchRequest(CommandStream.Node parent, String text, boolean regex) { return Service.FindRequest.newBuilder() .setCommandTreeNode(parent.getPath(Path.CommandTreeNode.newBuilder())).setText(text) .setIsRegex(regex).setMaxItems(1).setWrap(true).build(); } private ListenableFuture<Service.FindResponse> search(Service.FindRequest request) { SettableFuture<Service.FindResponse> result = SettableFuture.create(); client.streamSearch(request, result::set); return result; } protected void select(TreePath path) { models.commands.selectCommands(((CommandStream.Node) path.getLastSegment()).getIndex(), true); } @Override public Control getControl() { return this; } @Override public void reinitialize() { updateTree(false); } @Override public void onCaptureLoadingStart(boolean maintainState) { updateTree(true); } @Override public void onCaptureLoaded(Loadable.Message error) { if (error != null) { loading.showMessage(Error, Messages.CAPTURE_LOAD_FAILURE); } } @Override public void onCommandsLoaded() { updateTree(false); } @Override public void onCommandsSelected(CommandIndex index) { selectionHandler.updateSelectionFromModel(() -> getTreePath(index).get(), tree::setSelection); } @Override public void onContextsLoaded() { updateTree(false); } @Override public void onContextSelected(FilteringContext context) { updateTree(false); } @Override public void onThumbnailsChanged() { tree.refreshImages(); } private void updateTree(boolean assumeLoading) { if (assumeLoading || !models.commands.isLoaded()) { loading.startLoading(); tree.setInput(null); return; } loading.stopLoading(); tree.setInput(models.commands.getData()); if (models.commands.getSelectedCommands() != null) { onCommandsSelected(models.commands.getSelectedCommands()); } } private ListenableFuture<TreePath> getTreePath(CommandIndex index) { CommandStream.Node root = models.commands.getData(); ListenableFuture<TreePath> result = getTreePath(root, Lists.newArrayList(root), index.getNode().getIndicesList().iterator()); if (index.isGroup()) { // Find the deepest group/node in the path that is not the last child of its parent. result = Futures.transform(result, path -> { while (path.getSegmentCount() > 0) { CommandStream.Node node = (CommandStream.Node) path.getLastSegment(); if (!node.isLastChild()) { break; } path = path.getParentPath(); } return path; }); } return result; } private ListenableFuture<TreePath> getTreePath(CommandStream.Node node, List<Object> path, Iterator<Long> indices) { ListenableFuture<CommandStream.Node> load = models.commands.load(node); if (!indices.hasNext()) { TreePath result = new TreePath(path.toArray()); // Ensure the last node in the path is loaded. return (load == null) ? Futures.immediateFuture(result) : Futures.transform(load, ignored -> result); } return (load == null) ? getTreePathForLoadedNode(node, path, indices) : Futures.transformAsync(load, loaded -> getTreePathForLoadedNode(loaded, path, indices)); } private ListenableFuture<TreePath> getTreePathForLoadedNode(CommandStream.Node node, List<Object> path, Iterator<Long> indices) { int index = indices.next().intValue(); CommandStream.Node child = node.getChild(index); path.add(child); return getTreePath(child, path, indices); } private static class Tree extends LinkifiedTreeWithImages<CommandStream.Node, String> { private static final float COLOR_INTENSITY = 0.15f; protected final Models models; private final Widgets widgets; private final Map<Long, Color> threadBackgroundColors = Maps.newHashMap(); public Tree(Composite parent, Models models, Widgets widgets) { super(parent, SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI, widgets); this.models = models; this.widgets = widgets; } @Override protected ContentProvider<Node> createContentProvider() { return new ContentProvider<CommandStream.Node>() { @Override protected boolean hasChildNodes(CommandStream.Node element) { return element.getChildCount() > 0; } @Override protected CommandStream.Node[] getChildNodes(CommandStream.Node node) { return node.getChildren(); } @Override protected CommandStream.Node getParentNode(CommandStream.Node child) { return child.getParent(); } @Override protected boolean isLoaded(CommandStream.Node element) { return element.getData() != null; } @Override protected void load(CommandStream.Node node, Runnable callback) { models.commands.load(node, callback); } }; } @Override protected <S extends StylingString> S format(CommandStream.Node element, S string, Follower.Prefetcher<String> follower) { Service.CommandTreeNode data = element.getData(); if (data == null) { string.append("Loading...", string.structureStyle()); } else { if (data.getGroup().isEmpty() && data.hasCommands()) { string.append(Formatter.lastIndex(data.getCommands()) + ": ", string.defaultStyle()); API.Command cmd = element.getCommand(); if (cmd == null) { string.append("Loading...", string.structureStyle()); } else { Formatter.format(cmd, models.constants::getConstants, follower::canFollow, string, string.identifierStyle()); } } else { string.append(Formatter.firstIndex(data.getCommands()) + ": ", string.defaultStyle()); string.append(data.getGroup(), string.labelStyle()); long count = data.getNumCommands(); string.append(" (" + count + " command" + (count != 1 ? "s" : "") + ")", string.structureStyle()); } } return string; } @Override protected Color getBackgroundColor(CommandStream.Node node) { API.Command cmd = node.getCommand(); if (cmd == null) { return null; } long threadId = cmd.getThread(); Color color = threadBackgroundColors.get(threadId); if (color == null) { Control control = getControl(); RGBA bg = control.getBackground().getRGBA(); color = new Color(control.getDisplay(), lerp(getRandomColor(getColorIndex(threadId)), bg.rgb, COLOR_INTENSITY), bg.alpha); threadBackgroundColors.put(threadId, color); } return color; } private static int getColorIndex(long threadId) { // TODO: The index should be the i'th thread in use by the capture, not a hash of the // thread ID. This requires using the list of threads exposed by the service.Capture. int hash = (int) (threadId ^ (threadId >>> 32)); hash = hash ^ (hash >>> 16); hash = hash ^ (hash >>> 8); return hash & 0xff; } @Override protected boolean shouldShowImage(CommandStream.Node node) { return models.thumbs.isReady() && node.getData() != null && !node.getData().getGroup().isEmpty(); } @Override protected ListenableFuture<ImageData> loadImage(CommandStream.Node node, int size) { return noAlpha( models.thumbs.getThumbnail(node.getPath(Path.CommandTreeNode.newBuilder()).build(), size, i -> { /*noop*/ })); } @Override protected void createImagePopupContents(Shell shell, CommandStream.Node node) { LoadableImageWidget .forImage(shell, LoadableImage.newBuilder(widgets.loading).forImageData(loadImage(node, THUMB_SIZE)) .onErrorShowErrorIcon(widgets.theme)) .withImageEventListener(new LoadableImage.Listener() { @Override public void onLoaded(boolean success) { if (success) { Widgets.ifNotDisposed(shell, () -> { Point oldSize = shell.getSize(); Point newSize = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT); shell.setSize(newSize); if (oldSize.y != newSize.y) { Point location = shell.getLocation(); location.y += (oldSize.y - newSize.y) / 2; shell.setLocation(location); } }); } } }); } @Override protected Follower.Prefetcher<String> prepareFollower(CommandStream.Node node, Runnable cb) { return (node.getData() == null || node.getCommand() == null) ? nullPrefetcher() : models.follower.prepare(lastCommand(node.getData().getCommands()), node.getCommand(), cb); } @Override protected void follow(Path.Any path) { models.follower.onFollow(path); } @Override public void reset() { super.reset(); for (Color color : threadBackgroundColors.values()) { color.dispose(); } threadBackgroundColors.clear(); } } }