blob: c178174801a4dca7d408f24b6fb37683069cb392 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* 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.devrel.cluestick.studioclient;
import com.google.devrel.cluestick.searchservice.CluestickSearch;
import com.google.devrel.cluestick.searchservice.EventLog;
import com.appspot.cluestick_server.search.model.Result;
import com.intellij.CommonBundle;
import com.intellij.icons.AllIcons;
import com.intellij.ide.IdeBundle;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.DoubleClickListener;
import com.intellij.ui.OnePixelSplitter;
import com.intellij.ui.PopupHandler;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.SmartExpander;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
/**
* SearchResultsView extends JPanel to show the entire Cluestick sample widget.
*/
public class SearchResultsView extends JPanel implements Disposable {
private static final Logger LOG = Logger
.getInstance("#com.google.devrel.cluestick.studioclient.SearchResultsView");
private final EventLog eventLog;
private final Splitter splitter;
private final SearchResultsTree tree;
private final Browser browser;
private final JComponent secondComponent;
private Runnable closeAction;
public SearchResultsView(@NotNull final Project project, @NotNull final Symbol symbol,
@NotNull final List<Result> results) {
eventLog = new EventLog(ApplicationManager.getApplication().getService(CluestickSearch.class));
setLayout(new BorderLayout());
tree = new SearchResultsTree(symbol, results);
tree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
syncBrowser();
}
});
SmartExpander.installOn(tree);
browser = new CodeBrowser(project, eventLog);
splitter = new OnePixelSplitter(false, 0.5f);
splitter.setFirstComponent(ScrollPaneFactory.createScrollPane(tree));
secondComponent = ScrollPaneFactory.createScrollPane(browser.getPanel());
splitter.setSecondComponent(secondComponent);
secondComponent.setVisible(false); // initially hide browser
add(splitter, BorderLayout.CENTER);
createActionsPanel();
// Add action listeners. These listeners don't do anything if the target node isn't a leaf,
// because trunk nodes respond by opening/closing on these same events. Users should use the
// popup menu to open any relevant URLs instead.
new DoubleClickListener() {
@Override
protected boolean onDoubleClick(MouseEvent event) {
TreeNode node = getSingleSelectedNode();
TreePath path = tree.getClosestPathForLocation(event.getX(), event.getY());
if (node != null && node.isLeaf() && path != null && tree.isPathSelected(path)) {
invokeSelectAction(node);
return true;
}
return false;
}
}.installOn(tree);
tree.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
TreeNode node = getSingleSelectedNode();
if (node != null && node.isLeaf() && e.getKeyCode() == KeyEvent.VK_ENTER) {
invokeSelectAction(node);
}
}
});
DefaultActionGroup actionGroup = new DefaultActionGroup();
actionGroup.add(new OpenURLAction());
PopupHandler.installPopupMenu(tree, actionGroup, ActionPlaces.USAGE_VIEW_POPUP);
TreeUtil.expandAll(tree);
}
/**
* @param close The code to run when the Close button is pressed.
*/
public void setClose(Runnable close) {
this.closeAction = close;
}
private void invokeSelectAction(@Nullable TreeNode node) {
String url = getURLForNode(node);
if (url != null) {
openExternalBrowser(url);
}
}
@Nullable private String getURLForNode(@Nullable TreeNode node) {
if (node instanceof SearchResultsNode) {
String url = ((SearchResultsNode) node).getURL();
if (!StringUtil.isEmpty(url)) {
return url;
}
}
return null;
}
private void openExternalBrowser(@NotNull String url) {
LOG.info(String.format("Opening URL: `%s`", url));
// In nearly all cases, this will load the user's browser with the target URL. However,
// swallow its exceptions for sanity.
try {
Desktop.getDesktop().browse(new URI(url));
} catch (URISyntaxException ignored) {
// Thrown if the URL is bad.
} catch (IOException ignored) {
// Thrown if a browser can't be found or run.
} catch (RuntimeException ignored) {
// Thrown on a variety of other reasons, see-
// http://docs.oracle.com/javase/7/docs/api/java/awt/Desktop.html#browse(java.net.URI)
}
}
/**
* Create and add a JPanel containing default actions. This just includes Close for now.
*/
private void createActionsPanel() {
DefaultActionGroup group = new DefaultActionGroup();
group.add(new CloseAction());
ActionManager actionManager = ActionManager.getInstance();
JComponent actionsToolbar = actionManager
.createActionToolbar(ActionPlaces.CODE_INSPECTION, group, false).getComponent();
JPanel actionsPanel = new JPanel(new BorderLayout());
actionsPanel.add(actionsToolbar, BorderLayout.WEST);
add(actionsPanel, BorderLayout.WEST);
}
@Override
public void dispose() {
splitter.dispose();
eventLog.cancel();
if (browser instanceof Disposable) {
((Disposable) browser).dispose();
}
}
private TreeNode getSingleSelectedNode() {
TreeSelectionModel selectionModel = tree.getSelectionModel();
if (selectionModel.getSelectionCount() == 1) {
TreePath pathSelected = tree.getSelectionModel().getLeadSelectionPath();
if (pathSelected != null) {
return (TreeNode) pathSelected.getLastPathComponent();
}
}
return null;
}
private void syncBrowser() {
boolean visible = false;
TreeNode node = getSingleSelectedNode();
if (node instanceof SearchResultsNode) {
Result result = ((SearchResultsNode) node).getSearchResult();
if (result != null && showInBrowser(result)) {
visible = true;
}
}
if (!visible) {
browser.showEmpty();
}
if (secondComponent.isVisible() != visible) {
secondComponent.setVisible(visible);
splitter.doLayout();
splitter.doLayout(); // run twice, in case skipNextLayouting() was called on Splitter
}
}
/**
* Shows the given {@link Result} inside the current {@link Browser}.
* @param result The result to show.
* @return Whether the result could be shown.
*/
private boolean showInBrowser(@NotNull Result result) {
// TODO(thorogood): Make this part of Browser interface.
if (result.getCode() == null) {
return false;
}
Cursor currentCursor = getCursor();
setCursor(new Cursor(Cursor.WAIT_CURSOR));
browser.showResult(result);
setCursor(currentCursor);
return true;
}
private class OpenURLAction extends AnAction {
private OpenURLAction() {
super(IdeBundle.message("open.url.in.browser.tooltip"), null, null);
}
@Override
public void update(AnActionEvent e) {
String url = getURLForNode(getSingleSelectedNode());
e.getPresentation().setVisible(url != null);
}
@Override
public void actionPerformed(AnActionEvent e) {
invokeSelectAction(getSingleSelectedNode());
}
}
private class CloseAction extends AnAction implements DumbAware {
private CloseAction() {
super(CommonBundle.message("action.close"), null, AllIcons.Actions.Cancel);
}
@Override
public void actionPerformed(AnActionEvent e) {
if (closeAction != null) {
closeAction.run();
}
}
}
}